앞선 포스팅에서는 custom annotation 을 만들어서 조건별 validation 을 적용하는 방법을 알아보았다.
이번 포스팅에서는 @JsonTypeInfo 와 @JsonSubTypes 어노테이션을 이용하여 조건별 validation 을 적용하는 방법을 기록한다.
@JsonTypeInfo 와 @JsonSubTypes 는 특정 필드의 값에 따라서 Serialization, Deserialization 을 수행하는 클래스를 별도로 정의할 수 있다는 점에서 조건별로 validation 을 처리하는데 이용할 수 있을듯 하여 샘플을 만들어 보았다.
코드 구현
야구선수 등록 API 가 있고 야구선수 등록 데이터에는 uid, 이름, 선수타입, 타율, 홈런수, 방어율, 다승수 데이터가 있다.
선수타입에는 타자, 투수가 정의되어 있고 선수타입이 타자인 경우에는 타율, 홈런수 정보가 반드시 필요하고 투수인 경우에는 방어율, 다승수 정보가 반드시 필요하다.
DTO 정의
package com.example.conditional.validation.example.jsonsubtype.dto;
import com.example.conditional.validation.example.PlayerType;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.validation.constraints.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "type",
visible = true
)
@JsonSubTypes(
{
@JsonSubTypes.Type(value = HitterDTO.class, name = "HITTER"),
@JsonSubTypes.Type(value = PitcherDTO.class, name = "PITCHER")
}
)
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
public abstract class PlayerDTO {
@NotBlank
private String uid;
@NotBlank
private String name;
@NotNull
private PlayerType type;
}
PlayerDTO 의 type 값에 따라서 직렬화, 역직렬화 DTO 를 아래와 같이 정의하였다.
type 필드의 값 | DTO 클래스 |
HITTER | HitterDTO.class |
PITCHER | PitcherDTO.class |
package com.example.conditional.validation.example.jsonsubtype.dto;
import com.example.conditional.validation.example.PlayerType;
import jakarta.validation.constraints.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
@NoArgsConstructor
public class HitterDTO extends PlayerDTO{
//타율 (타자)
@NotNull
@DecimalMin("0.000") @DecimalMax("1.000")
private BigDecimal battingAverage;
//홈런 (타자)
@NotNull
@Min(0) @Max(100)
private Integer homeruns;
@Builder
public HitterDTO(String uid, String name, PlayerType type,
BigDecimal battingAverage, Integer homeruns) {
super(uid, name, type);
this.battingAverage = battingAverage;
this.homeruns = homeruns;
}
}
package com.example.conditional.validation.example.jsonsubtype.dto;
import com.example.conditional.validation.example.PlayerType;
import jakarta.validation.constraints.*;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
public class PitcherDTO extends PlayerDTO{
//방어율 (투수)
@NotNull
@DecimalMin("0.000") @DecimalMax("10.000")
private BigDecimal era;
//다승 (투수)
@NotNull
@Min(0) @Max(100)
private Integer wins;
@Builder
public PitcherDTO(String uid, String name, PlayerType type,
BigDecimal era, Integer wins) {
super(uid, name, type);
this.era = era;
this.wins = wins;
}
}
Controller 정의
package com.example.conditional.validation.example.jsonsubtype.controller;
import com.example.conditional.validation.example.jsonsubtype.dto.PlayerDTO;
import com.example.conditional.validation.example.jsonsubtype.service.PlayerService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(path = "/jsonsubtype")
@RequiredArgsConstructor
public class PlayerController {
private final PlayerService playerService;
@PostMapping(path = "/player", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<PlayerDTO> createPlayer(
@Validated
@RequestBody PlayerDTO playerDTO) {
PlayerDTO playerDTO1 = playerService.create(playerDTO);
return ResponseEntity.ok(playerDTO1);
}
}
Service 정의
package com.example.conditional.validation.example.jsonsubtype.service;
import com.example.conditional.validation.example.jsonsubtype.dto.PlayerDTO;
import org.springframework.stereotype.Service;
@Service
public class PlayerService {
public PlayerDTO create(PlayerDTO playerDTO) {
return playerDTO;
}
}
ControllerAdvicer
package com.example.conditional.validation.example;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
@Slf4j
public class ControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException e, HttpServletRequest request) {
Map<String, String> errorDefaultMessages = new HashMap<>();
Map<String, Object> errors = new HashMap<>();
String objectName = e.getObjectName();
e.getFieldErrors()
.forEach(error -> errorDefaultMessages.put(error.getField(), error.getDefaultMessage()));
log.error("failure check request validation, uri: {}, objectName: {}, {}", request.getRequestURI(), objectName, errors);
return ResponseEntity.badRequest().body(errorDefaultMessages);
}
}
테스트 코드
package com.example.conditional.validation.example.jsonsubtype.controller;
import com.example.conditional.validation.example.PlayerType;
import com.example.conditional.validation.example.jsonsubtype.dto.HitterDTO;
import com.example.conditional.validation.example.jsonsubtype.dto.PitcherDTO;
import com.example.conditional.validation.example.jsonsubtype.dto.PlayerDTO;
import com.example.conditional.validation.example.jsonsubtype.service.PlayerService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import java.math.BigDecimal;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = PlayerController.class)
@Import(PlayerService.class)
class PlayerControllerTest {
@Autowired
private MockMvc mockMvc;
@DisplayName("타자 등록 유효성 체크 성공 테스트")
@Test
void create_hitter_validation_success_test() throws Exception {
PlayerDTO playerDTO = HitterDTO.builder()
.uid(UUID.randomUUID().toString())
.name("hitter1")
.type(PlayerType.HITTER)
.battingAverage(new BigDecimal("0.400"))
.homeruns(50)
.build();
ObjectMapper objectMapper = new ObjectMapper();
mockMvc.perform(
post("/jsonsubtype/player")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(playerDTO))
)
.andExpect(status().isOk())
.andDo(print());
}
@DisplayName("투수 등록 유효성 체크 성공 테스트")
@Test
void create_pitcher_validation_success_test() throws Exception {
PlayerDTO playerDTO = PitcherDTO.builder()
.uid(UUID.randomUUID().toString())
.name("pitcher")
.type(PlayerType.PITCHER)
.era(new BigDecimal("0.235"))
.wins(10)
.build();
ObjectMapper objectMapper = new ObjectMapper();
mockMvc.perform(
post("/jsonsubtype/player")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(playerDTO))
)
.andExpect(status().isOk())
.andDo(print());
}
@DisplayName("타자 등록 유효성 체크 실패 테스트 - 홈런정보 누락")
@Test
void create_hitter_validation_fail_test() throws Exception {
PlayerDTO playerDTO = HitterDTO.builder()
.uid(UUID.randomUUID().toString())
.name("hitter1")
.type(PlayerType.HITTER)
.battingAverage(new BigDecimal("0.400"))
.build();
ObjectMapper objectMapper = new ObjectMapper();
mockMvc.perform(
post("/jsonsubtype/player")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(playerDTO))
)
.andExpect(status().isBadRequest())
.andDo(print());
}
@DisplayName("투수 등록 유효성 체크 실패 테스트 - 다승정보 누락")
@Test
void create_pitcher_validation_fail_test() throws Exception {
PlayerDTO playerDTO = PitcherDTO.builder()
.uid(UUID.randomUUID().toString())
.name("pitcher1")
.type(PlayerType.PITCHER)
.era(new BigDecimal("0.235"))
.build();
ObjectMapper objectMapper = new ObjectMapper();
mockMvc.perform(
post("/jsonsubtype/player")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(playerDTO))
)
.andExpect(status().isBadRequest())
.andDo(print());
}
@DisplayName("타자 등록 유효성 체크 실패 테스트 - 홈런 개수 limit")
@Test
void create_hitter_validation_limit_fail_test() throws Exception {
PlayerDTO playerDTO = HitterDTO.builder()
.uid(UUID.randomUUID().toString())
.name("hitter1")
.type(PlayerType.HITTER)
.battingAverage(new BigDecimal("0.400"))
.homeruns(1000)
.build();
ObjectMapper objectMapper = new ObjectMapper();
mockMvc.perform(
post("/jsonsubtype/player")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(playerDTO))
)
.andExpect(status().isBadRequest())
.andDo(print());
}
@DisplayName("투수 등록 유효성 체크 실패 테스트 - 다승 개수 limit")
@Test
void create_pitcher_validation_limit_fail_test() throws Exception {
PlayerDTO playerDTO = PitcherDTO.builder()
.uid(UUID.randomUUID().toString())
.name("pitcher1")
.type(PlayerType.PITCHER)
.era(new BigDecimal("0.235"))
.wins(1000)
.build();
ObjectMapper objectMapper = new ObjectMapper();
mockMvc.perform(
post("/jsonsubtype/player")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(playerDTO))
)
.andExpect(status().isBadRequest())
.andDo(print());
}
@DisplayName("타자 생성시 투수 정보도 입력했을 때 테스트")
@Test
void create_player_include_all_fields_test() throws Exception {
com.example.conditional.validation.example.annotation.dto.PlayerDTO playerDTO =
com.example.conditional.validation.example.annotation.dto.PlayerDTO
.builder()
.uid(UUID.randomUUID().toString())
.name("hitter1")
.type(PlayerType.HITTER)
.battingAverage(new BigDecimal("0.300"))
.homeruns(50)
.era(new BigDecimal("0.000"))
.wins(20)
.build();
ObjectMapper objectMapper = new ObjectMapper();
mockMvc.perform(
post("/jsonsubtype/player")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(playerDTO))
)
.andExpect(status().isOk())
.andDo(print());
}
}
테스트 결과는 아래와 같다.
필드의 값 조건에 따라서 유효성 검사를 필드가 다른 경우 @JsonTypeInfo, @JsonSubTypes 를 통해서 처리하는 방법을 알아 보았다.
annotation 정의 방법에 비해서 DTO 클래스를 잘 구분지어서 정의하면 조금 더 손쉽게 처리할 수 있을듯 하다.
git 샘플코드
https://gitlab.com/blog4031530/spring-conditional-validation-example
'스프링부트' 카테고리의 다른 글
spring boot resource 파일 access (0) | 2023.08.27 |
---|---|
spring-boot-starter-parent 와 spring-boot-dependencies (0) | 2023.08.15 |
spring boot 필드값 조건별 validation 하기 - custom annotation (0) | 2023.07.29 |
Spring REST docs + asciidoctor + restdoc spec + swagger ui 를 통한 문서 자동화 - 필드 제약사항 추가 (커스텀) (0) | 2023.07.28 |
Spring REST docs + asciidoctor + restdoc spec + swagger ui 를 통한 문서 자동화 - 기본 샘플 코드 (0) | 2023.07.22 |
댓글