스프링 MVC 2편(백엔드 웹 개발 활용 기술)

Ch04. 검증(Validation) - Validator 분리

webmaster 2022. 3. 15. 15:24
728x90

Controller에 Validation 분리

  • Validation을 Controller에 모여 있어 실제 Controller 로직을 기능을 찾기 힘들다.
  • Validation 로직을 실제 새로운 Class를 만들어 분리시켜 동작시켜 보자

ItemValication.class

@Component
public class ItemValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        //item == clazz
        return Item.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        Item item = (Item) target;

        //ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");
        //검증 로직
        if (!StringUtils.hasText(item.getItemName())) {
            //objectName 같은 경우 bindingResult 가 이미 알고있어서 쓰지 않아도 된다.
            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() >= 9999) {
            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);
            }
        }

    }
}

addItemV5

@PostMapping("/add")
public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult,
    RedirectAttributes redirectAttributes, Model model) {
    itemValicator.validate(item, bindingResult);
    //검증에 실패하면 다시 입력 폼으로
    if (bindingResult.hasErrors()) {
        log.info("bindingResult = {}", 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}";
}
  • supports() {} : 해당 검증 기를 지원하는 여부 확인(뒤에서 설명)
  • validate(Object target, Errors errors) : 검증 대상 객체와 BindingResult
  • ItemValidator를 스프링 빈으로 주입받아서 직접 호출했다.

WebDataBinder를 통해 사용하기

private final ItemValidator itemValidator;

@InitBinder
public void init(WebDataBinder dataBinder){
    dataBinder.addValidators(itemValidator);
}
@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult,
    RedirectAttributes redirectAttributes, Model model) {
    //검증에 실패하면 다시 입력 폼으로
    if (bindingResult.hasErrors()) {
        log.info("bindingResult = {}", 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}";
}
  • validator를 직접 호출하는 부분이 사라지고, 대신에 검증 대상 앞에 @Validated 가 붙었다.
  • @Validated는 검증 기를 실행하라는 애노테이션이다.
    • 이 애노테이션이 붙으면 앞서 WebDataBinder에 등록한 검증 기를 찾아서 실행한다. 그런데 여러 검증 기를 등록한다면 그중에 어떤 검증기가 실행되어야 할지 구분이 필요하다. 이때 supports()가 사용된다
  • 글로벌 설정 - 모든 컨트롤러에 다 적용
@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
 public static void main(String[] args) {
 SpringApplication.run(ItemServiceApplication.class, args);
 }
 @Override
 public Validator getValidator() {
 return new ItemValidator();
 }
}
  • 기존 컨트롤러의 @InitBinder 를 제거해도 글로벌 설정으로 정상 동작하는 것을 확인할 수 있다
  • 글로벌 설정을 하면 다음에 설명할 BeanValidator가 자동 등록되지 않는다. 글로벌 설정 부분은 주석처리해두자. 참고로 글로벌 설정을 직접 사용하는 경우는 드물다.
  • 참고
    • 검증시 @Validated @Valid 둘 다 사용 가능하다. 
    • javax.validation.@Valid 를 사용하려면 build.gradle 의존관계 추가가 필요하다.
    • implementation 'org.springframework.boot:spring-boot-starter-validation' 
    • @Validated 는 스프링 전용 검증 애노테이션이고, @Valid는 자바 표준 검증 애노테이션이다. 
728x90