@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Item item = (Item) target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "itemName", "required");
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
errors.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
}
if (item.getQuantity() == null || item.getQuantity() > 10000) {
errors.rejectValue("quantity", "max", new Object[]{9999}, null);
}
//특정 필드 예외가 아닌 전체 예외
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
}
}
검증 기능을 매번 코드로 작성하는 것은 굉장히 귀찮다.
근데 생각해보면 필드에 대한 일반적인 검증 로직은 대체로 정해져있다.
검증 로직을 모든 프로젝트에 적용할 수 있게 공통화하고, 표준화 한 것이 바로 Bean Validation
이다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
Bean Validation을 사용하려면 build.gradle에 위의 의존성을 추가해야한다.
@Data
public class Item {
private Long id;
@NotBlank // "" or " " or null 불가
private String itemName;
@NotNull // null 불가
@Range(min = 1000, max = 1000000) // 1000 <= x <= 1000000
private Integer price;
@NotNull
@Max(9999) // x <= 9999
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
Item객체에 Bean Validation이 제공하는 여러 검증 어노테이션을 붙인다.
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v3/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v3/items/{itemId}";
}
@Valid
나 @Validated
만 붙여주면 알아서 검증 로직이 실행된다.Bean Validation에서 특정 필드(FieldError)가 아닌 해당 오브젝트 관련 오류(ObjectError)는 어떻게 처리할 수 있을까?
다음과 같이 @ScriptAssert() 를 사용하면 된다.
@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
public class Item {
// ...
}
그런데 실제 사용해보면 제약이 많고 복잡하다. 그리고 실무에서는 검증 기능이 해당 객체의 범위를 넘어서는 경우들도 종종 등장하는데, 그런 경우 대응이 어렵다.
따라서 오브젝트 오류(글로벌 오류)의 경우 @ScriptAssert 을 억지로 사용하는 것 보다는 다음과 같이 오브젝트 오류 관련 부분만 직접 자바 코드로 작성하는 것을 권장한다.
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item,
BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// 특정 필드 예외가 아닌 전체 예외
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);} }
// 특정 필드 예외
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v3/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v3/items/{itemId}";
}
BeanValidator는 바인딩에 성공한 필드에만 Bean Validation을 적용한다
(바인딩에 실패한 필드는 BeanValidation을 적용하지 않는다.)
@Valid
, @Validated
는 HttpMessageConverter(@RequestBody)에도 적용할 수 있다.
참고
- @ModelAttribute : HTTP 요청 파라미터(URL 쿼리 스트링, POST Form) 처리
- @RequestBody : HTTP Body의 데이터를 객체로 변환할 때 (주로 API JSON 요청을 다룰 때)
@PostMapping("/add")
public Object addItem(@RequestBody @Validated ItemSaveForm form,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return bindingResult.getAllErrors();
}
// 성공로직
return form;
}
API의 경우 3가지 경우를 나누어 생각해야 한다.
1) 성공 요청: 성공
2) 실패 요청: JSON을 객체로 생성하는 것 자체가 실패함
3) 검증 오류 요청: JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패함
2)의 경우
.w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error:
Cannot deserialize value of type `java.lang.Integer` from String "A": not a valid Integer value;
nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException:
Cannot deserialize value of type `java.lang.Integer` from String "A": not a valid Integer value at [Source: (PushbackInputStream); line: 1, column: 30] (through reference chain: hello.itemservice.domain.item.Item["price"])]
3)의 경우
HTTP 요청 파리미터를 처리하는 @ModelAttribute
HTTP Body를 처리하는 @RequestBody (HttpMessageConverter)
이 글은 인프런 김영한님의 스프링 MVC2의 [섹션5. 검증2 - Bean Validation]을 정리한 글입니다.