💻본 포스팅은 '스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 김영한'님의 강의를 듣고 작성되었습니다.
[목차]
0. 들어가기에 앞서..
1. Validator 분리(1)
2. Validator 분리(2)
[0. 들어가기에 앞서..]
컨트롤러와 검증을 분리하는 이유가 무엇일까?
- 컨트롤러 안에 검증 로직이 차지하는 부분이 많아지게 되면 분리해서 사용하는 것이 바람직하다.
- 분리하지 않고 컨트롤러 안에서 수많은 검증 로직을 사용하면 컨트롤러가 너무 많은 역할을 차지하게 되기 때문에 분리해 사용하는 것이 좋다.
- 또한 분리한 검징 로직은 재사용할 수도 있게 된다.
- 따로 클래스를 사용해서 validation 관련 코드를 몰아넣어주자.
[1. Validator 분리(1)]
package hello.itemservice.web.validation;
import hello.itemservice.domain.item.Item;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
@ComponentScan
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz);
//자식 클래스까지 전부 커버 가능.
}
@Override
public void validate(Object target, Errors errors) {
//검증 로직
//Object로 넘어오기 때문에 casting을 해주어야 한다.
Item item = (Item) target;
//특정 필드 예외
if (!StringUtils.hasText(item.getItemName())) {
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);
}
}
}
}
- 컨트롤러가 아닌 따로 클래스를 생성해서 그 안에 넣어준 뒤, 컨트롤러에서 불러 사용하는 방식으로 검증을 진행하고 있다.
- 이때 validator 인터페이스를 상속받아 구현해서 사용하는 것이 핵심이다.
@PostMapping("/add")
public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
itemValidator.validate(item,bindingResult);
//검증에 실패하게 될 때 다시 입력 폼으로 간다.
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}";
}
- itemValidator.validate(item,bindingResult); -검증 클래스 직접사용
private final ItemValidator itemValidator;
- 의존성 주입도 잊지말고 해주자.
- 따로 위의 방법대로 하지 않고 필요할 때마다 객체생성해서 진행해도 되지만 싱글톤이 보장되지 않기때문에 비추.
[2. Validator 분리(2)]
@PostMapping("/add")
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}";
}
- 직접 검증 클래스를 호출해서 사용하는 것이 아닌 @Valiated 애너테이션을 검증 대상 앞에 붙여 사용하는 방식으로 기존 방식과 동일하게 동작한다.
- @Validated
- 검증기를 실행하라는 애노테이션이다.
- 이 애노테이션이 붙으면 WebDataBinder에 등록한 검증기를 찾아 실행한다.
- 여러 검증기를 등록할 땐?
- supports()를 사용해 어떤 검증기를 실행할지를 구분한다.
- supports(Item.class) 호출 -> 결과가 true -> ItemValidator의 validate()가 호출된다.