Validator 분리
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
// Item 및 Item의 자식의 타입인지 확인한다.
// 자식까지 확인하기 위해 isAssignableFrom()을 쓴다.
return Item.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
// Object 타입이기 때문에 캐스팅을 해야 한다.
Item item = (Item) target;
if (!StringUtils.hasText(item.getItemName())) {
// Errors의 자식이 BidingResult라서 담을 수 있다.
errors.rejectValue("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);
}
}
}
}
검증 로직을 분리해 컨트롤러의 코드를 깔끔하게 유지할 수 있다.
supports()
검증기를 지원하는지 확인한다.
validate(Object target, Errors errors)
검증 대상 객체와 BindingResult를 파라미터로 받는다.
WebDataBinder
@Slf4j
@Controller
@RequestMapping("/validation/v2/items")
@RequiredArgsConstructor
public class ValidationItemControllerV2 {
private final ItemRepository itemRepository;
private final ItemValidator itemValidator;
@InitBinder
public void init(WebDataBinder webDataBinder) {
webDataBinder.addValidators(itemValidator);
}
@PostMapping("/add")
// @Validated 애너테이션을 추가한다.
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
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}";
}
}
@InitBinder가 설정된 컨트롤러의 어떤 메서드든 호출될 때마다 dataBinder가 항상 새롭게 만들어져 적용된다. validator 코드를 삭제하고 애너테이션만 달아도 검증이 작동한다.
동작 방식
@Validator 애너테이션이 붙으면 WebDataBinder에 등록한 검증기를 찾아 실행한다.
여러 검증기를 등록한다면 supports()를 사용해 어떤 검증기가 실행될지 확인한다.
앞서 정의한 supports()를 호출하면 Item 타입인지 확인한다.
결과가 true이므로 ItemValidator의 validate()가 호출된다.
글로벌 설정
@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Override
public Validator getValidator() {
return new ItemValidator();
}
}
모든 컨트롤러에 다 적용하고 싶다면 글로벌 설정이 가능하다. 따로 정의된 게 있다면 supports()가 getValidator()보다 먼저 호출된다.
참고
@Validated
스프링 전용 검증 애너테이션
@Valid
자바 표준 검증 애너테이션
@Validated, @Valid 모두 검증할 때 사용 가능하다. 하지만 후자는 org.springframework.boot:spring-boot-starter-validation
의존성이 필요하다.
Last updated
Was this helpful?