검증 - Validation (2)

2022. 3. 31. 21:32Spring 기초

이전 검증 - Validation (1) 포스팅 참고

Controller에서 rejectValue()메서드를 활용해 한층 깔끔해졌다고 해도, 여전히 Controller에는 검증하는 코드가 한가득 있다.
복잡한 검증 로직을 따로 분리하자.

ItemValidator를 만들어보자

@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);
 		}
 	}
 }
}

스프링은 검증을 체계적으로 제공하기 위해 다음 인터페이스를 제공한다.

public interface Validator { 
	boolean supports(Class clazz); 
    void validate(Object target, Errors errors); 
 }

supports() {} : 해당 검증기를 지원하는 여부 확인(뒤에서 설명)
validate(Object target, Errors errors) : 검증 대상 객체와 BindingResult

따로 분리한 ItemValidator를 컨트롤러에서 직접호출 해보자.

private final ItemValidator itemValidator; (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";
 }

실행해보면 기존과 완전히 동일하게 동작하는 것을 확인할 수 있다. 검증과 관련된 부분이 깔끔하게 분리되었다.

 

스프링이 Validator 인터페이스를 별도로 제공하는 이유는 체계적으로 검증 기능을 도입하기 위해서다.
그런데 앞에서는 검증기를 직접 호출해서 사용했고, 이렇게 사용해도 괜찮다.
그런데 Validator 인터페이스를 사용해서 검증기를 만들면 스프링의 추가적인 도움을 받을 수 있다!!

WebDataBinder를 통해서 사용하기
WebDataBinder 는 스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다.

컨트롤러에 다음과 같은 코드를 추가했다.

@InitBinder
public void init(WebDataBinder dataBinder) {
     log.info("init binder {}", dataBinder);
     dataBinder.addValidators(itemValidator);
}

이렇게 WebDataBinder 에 검증기를 추가하면 해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있다.
@InitBinder 어노테이션을 사용하면 해당 컨트롤러에만 영향을 준다. 참고로 글로벌 설정은 별도로 해야한다. (마지막에 설명하겠다!)

 

그리고 다음과 같이 컨트롤러를 변경했다.

@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}";
}

validator를 직접 호출하는 부분이 사라지고, 대신에 검증 대상 앞에 @Validated 가 붙었다.

 

동작 방식

@Validated 는 검증기를 실행하라는 애노테이션이다.
이 애노테이션이 붙으면 앞서 WebDataBinder 에 등록한 검증기를 찾아서 실행한다. 그런데 여러 검증기를 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다.
이때 supports() 가 사용된다. 여기서는 supports(Item.class) 호출되고, 결과가 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();
 }
}

 


참고로
검증시 @Validated @Valid 둘다 사용가능하다. 
javax.validation.@Valid 를 사용하려면 build.gradle 의존관계 추가가 필요하다.
implementation 'org.springframework.boot:spring-boot-starter-validation' 
@Validated 는 스프링 전용 검증 애노테이션이고, @Valid 는 자바 표준 검증 애노테이션이다. 

'Spring 기초' 카테고리의 다른 글

검증 - Bean Validation(HTTP 메시지 컨버터)  (0) 2022.04.02
검증 - Bean Validation  (0) 2022.04.02
검증 - Validation(1)  (0) 2022.03.31
REST API와 Ajax(2)  (0) 2022.03.27
REST API와 Ajax(1)  (0) 2022.03.27