@PostMapping("/add")
public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName", "required");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1_000_000) {
bindingResult.rejectValue("price", "range", new Object[]{1000, 1_000_000}, null);
}
if (item.getQuantity() == null || item.getQuantity() >= 999) {
bindingResult.rejectValue("quantity", "max", new Object[]{9999}, null);
}
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10_000) {
bindingResult.reject("totalPriceMin", new Object[]{10_000, resultPrice}, null);
}
}
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v2/addForm";
}
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
위 코드는 컨트롤러의 1개의 api에 대한 코드를 가져온 것이다. 굉장히 길진 않지만 해당 코드에서 75%를 넘게 검증로직이 자리한 것을 볼 수 있다.
그리고 모든 form데이터가 넘어오는 post마다 이짓거리를 하면 중복되는 코드도 굉장히 많을 것이다.
그래서 이제 컨트롤러의 역할을 쪼개보자.
@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;
if (!StringUtils.hasText(item.getItemName())) {
errors.rejectValue("itemName", "required");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1_000_000) {
errors.rejectValue("price", "range", new Object[]{1000, 1_000_000}, null);
}
if (item.getQuantity() == null || item.getQuantity() >= 999) {
errors.rejectValue("quantity", "max", new Object[]{9999}, null);
}
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10_000) {
errors.reject("totalPriceMin", new Object[]{10_000, resultPrice}, null);
}
}
}
}
support()
isAssignableFrom()
메서드로 js
의 isInstanceof
와 같은 역할을 한다.validate()
Object
로 해놨기 때문에 원하는 객체로 casting
해야한다.Errors
는 바로 bindingResult
의 부모 class이다.`@Component``
...
@RequiredArgsConstructor
public class ValidationItemControllerV2 {
private final ItemRepository itemRepository;
private final ItemValidator itemValidator;
...
support()
로 지원하는 개체인지 확인하고 validate()
함수에 넣어주면 된다.@InitBinder
public void init(WebDataBinder dataBinder) {
log.info("init binder {}", dataBinder);
dataBinder.addValidators(itemValidator);
}
WebDataBinder
이는 해당 컨트롤러가 호출될 때 마다 자동으로 새로 만들어진다.
WebDataBinder 는 스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다
그리고 해당 코드를 실행하여 검증기를 항상 생성하여 갖고있다.
@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v2/addForm";
}
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
그러면 spring이 알아서 자동으로 @Validated
어노테이션을 보고 검증을 자동으로 돌려 해당 결과값을 bindingResult
에 넣어둔다.
동작과정
@Validated
는 검증기를 실행하라는 애노테이션이다.
이 애노테이션이 붙으면 앞서 WebDataBinder
에 등록한 검증기를 찾아서 실행한다.
그런데 여러 검증기를 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다.
바로 이때 supports()
가 사용된다. 여기서는 바로뒤 @ModelAttribute
가 Item이니까 "supports(Item.class)"가 호출되고, 결과가 true 이므로 spring validator를 implement한 "ItemValidator" 의 validate()
가 호출된다.
xxxApplication
으로 가서@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Override
public Validator getValidator() {
return new ItemValidator();
}
}
WebMvcConfigurer
를 구현하고 getValidator()
메서드를 오버라이드 해주면 된다.
그러게 된다면 각 컨트롤러에서 dataBinder
에 addValidator()
를 하여 검증기를 안 넣어줘도 된다.
그런데 사실 글로벌설정을 할 일은 거의 없을 것이다.
왜냐하면 BeanValidator
때문인데 이는 다음 포스팅에 적도록 하겠다.
참고로 검증시
@Validated
,@Valid
둘다 사용가능하다.
javax.validation.@Valid
를 사용하려면build.gradle
의존관계 추가가 필요하다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
요거.
@Validated
는 스프링 전용 검증 애노테이션이고,
@Valid
는 자바 표준 검증 애노테이션이다.