2022. 7. 3. 21:13ㆍLanguage`/Spring
앞에서 검증 기능을 직접 작성함으로써 객체에 대한 검증을 하였다
그런데 검증 기능을 매번 코드로 작성하려면 엄청난 시간과 노력을 투자해야 한다
그리고 대부분의 검증 로직은 {빈 값인지 아닌지 / 특정 크기를 넘는지 아닌지 / ...} 등 매우 일반적인 로직이다
이러한 검증 로직을 모든 프로젝트에 적용할 수 있게 공통화하고, 표준화 한 것이 바로 "Bean Validation"이다
>> Bean Validation을 활용하면 "애노테이션" 하나로 검증 로직을 매우 편리하게 적용할 수 있다
Bean Validation을 구현한 기술중에서 일반적으로 사용하는 구현체는 Hibernate Validator이다
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ItemDTO {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000000)
private Integer price;
@NotNull
@Min(5)
private Integer quantity;
}
@NotBlank : 빈값 + 공백의 경우 에러처리
@NotNull : null일 경우 에러처리
@Range : {min ≤ "value" ≤ max}
@Min : 최소 값
@Max : 최댓값
....
Bean Validation 검증 순서
1. @ModelAttribute를 통해서 각 Request Data를 객체에 매핑시켜서 넣어주기
- 성공 : next level
- 실패 : typeMissmatch로 FieldError 추가
2. Validator 적용
과정 (1)에서 typeMissmatch가 발생하게되면 애초에 타입이 맞지 않는 data가 들어온것이기 때문에 Validator가 동작하지 않는다
Bean Validation 에러 메시지 찾는 순서
1. messageSource에서 메시지 찾기
- ~~~.properties에 설정한 메세지들
2. Annotation에 설정한 message
3. 라이브러리가 제공하는 기본 값
Bean Validation - groups
일반적으로 회원 등록/회원 수정의 경우 검증을 다르게 해야 한다
- 회원 등록은 모든 form data에 대해서 검증을 해야하지만, 회원 수정은 일반적으로 특정 data를 변경하기 때문에
이 때 groups라는 속성을 사용하면 서로 다른 타입의 검증을 시도할 수 있다
public interface ItemSaveForm {
}
public interface ItemEditForm {
}
일단 groups를 지정하는데 사용할 2개의 인터페이스를 만들어주자
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Item {
private Long id;
@NotBlank(groups = {ItemSaveForm.class, ItemEditForm.class})
private String itemName;
@NotNull(groups = {ItemSaveForm.class, ItemEditForm.class})
@Range(min = 1000, max = 1000000000, groups = {ItemSaveForm.class, ItemEditForm.class})
private Integer price;
@NotNull(groups = {ItemSaveForm.class, ItemEditForm.class})
@Min(value = 5, groups = ItemSaveForm.class)
private Integer quantity;
}
<itemName>
@NotBlank : 등록/수정 둘다 동일하게 검증
<price>
@NotNull : 등록/수정 둘다 동일하게 검증
@Range : 등록/수정 둘다 동일하게 검증
<quantity>
@NotNull : 둥록/수정 둘다 동일하게 검증
@Min : 등록할 때만 검증 & 수정할 때는 검증 X
@PostMapping("/add")
public String addItem(
@Validated(ItemSaveForm.class) @ModelAttribute Item item,
BindingResult bindingResult
)
@PostMapping("/{itemId}/edit")
public String editItem(
@PathVariable Long itemId,
@Validated(ItemEditForm.class) @ModelAttribute Item item,
BindingResult bindingResult
)
이처럼 검증을 서로 다르게 수행할 수 있다
Bean Validation - 검증용 DTO
실무에서는 거의 원래 도메인이 아닌 검증용 DTO를 따로 만들어서 검증 로직을 작성한다
왜냐하면 각 상황마다 검증할 data가 다르고 이를 도메인에 그대로 검증을 시켜버리면 원하는 결과를 얻기 힘들기 때문이다
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ItemDTO {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000000)
private Integer price;
@NotNull
@Min(5)
private Integer quantity;
}
@RequestMapping("/items")
public String addItem(
@Validated @ModelAttribute ItemDTO item,
BindingResult bindingResult
이처럼 아예 따로 만들어서 검증할 대상 객체도 검증용 DTO로 설정하면 된다
<Domain 객체 그대로 사용>
장점 : 중간에 따로 parsing을 해서 만들 필요가 없이 그대로 사용하면 된다
단점 : 간단한 경우에만 적용이 가능하고, 검증이 중복되면 groups를 사용해야 한다
<검증용 객체 따로 생성>
장점 : 전송받는 폼 데이터가 복잡해도 거기에 맞는 별도의 폼 객체를 사용해서 데이터를 받을 수 있다
단점 : parsing과정이 추가된다
Bean Validation - HTTP Message Converter
@Valid, @Validate는 @RequestBody & HttpEntity에도 적용할 수 있다
HTTP API가 들어오는 경우는 크게 3가지로 분류할 수 있다
1. 요청 : 실패
이 경우는 JSON → 객체로 변환하는 자체가 실패한 것이다
- 데이터 type 불일치로 인해서 Client가 JSON Data를 잘못 보낸 것이다
2. 요청 : 검증 오류
이 경우는 JSON → 객체로 변환은 성공했지만, 내부의 값들에 대한 검증이 실패한 경우이다
3. 요청 : 성공
이 경우 JSON → 객체로 변환도 성공하고 내부 값들에 대한 검증도 성공한 경우이다
@ModelAttribute vs @RequestBody
@ModelAttribute는 field단위로 검증이 적용된다. 따라서 어느 field의 type이 맞지 않는 경우에도 다른 field에게 영향을 주지 않는다
반면에 @RequestBody는 HTTP API(JSON)으로 데이터가 들어오는 경우이고 일단 검증을 하려면 먼저 "JSON → 객체"로 변환이 먼저 되어야 한다
근데 여기서 어느 field의 type이 맞지 않는 경우는 애초에 객체로 변환이 실패하기 때문에 검증은 들어갈 수도 없다
따라서 API 요청에 대한 검증을 하려면 일단 먼저 JSON → 객체로의 변환이 성공됨을 전제로 해야 한다
만약 변환이 실패하였다면 애초에 Client가 Data를 잘못보낸것이기 떄문에 4xx Error를 return해주면 된다