인프런/스프링 MVC 2편

9) Validator , @Validated (@Valid)

backend dev 2023. 2. 4.

검증로직  Validator를 만들어서 분리

 

복잡한 검증 로직을 별도로 분리하자.

 

컨트롤러에서 검증 로직이 차지하는 부분은 매우 크다.

 @PostMapping("/add")
    public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

        if (bindingResult.hasErrors()) {
            log.info("erros = {}", bindingResult);
//            log.info("model ={}", model);
            return "validation/v2/addForm";
        }

        log.info("objectName={}", bindingResult.getObjectName());
        log.info("target={}", bindingResult.getTarget());

        if (!StringUtils.hasText(item.getItemName())) {
            bindingResult.rejectValue("itemName", "required");
        }

        if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
            bindingResult.rejectValue("price","range",new Object[]{1000, 1000000}, null);
        }

        if (item.getQuantity() == null || item.getQuantity() >= 9999) {
            bindingResult.rejectValue("quantity","max",new Object[]{9999}, null);
        }


        if (item.getPrice() != null && item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice < 10000) {
                bindingResult.reject("totalPriceMin",new Object[]{10000, resultPrice}, null);
            }
        }

이런 경우 별도의 클래스로 역할을 분리하는 것이 좋다. 그리고 이렇게 분리한 검증 로직을 재사용 할 수도 있다

(item 를 사용하는 비슷한 컨트롤러가 있다면 재사용가능)

 

그리고 컨트롤러메소드가 깔끔해진다.

 

 

item객체 검증용 ItemValidator 생성

 

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

implements해서 구현해서 사용하면 된다.

@Component //스프링빈에 등록시켜서 컨트롤러에서 쉽게 가져다 쓰기위함
public class ItemValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return Item.class.isAssignableFrom(clazz);
        //매개변수로 들어오는 클래스타입과  item 클래스타입이 같은지 체크하는 부분
        //매개변수로 들어오는 클래스타입이 item 클래스의 자식클래스인지 체크하는 부분
    }

    @Override
    public void validate(Object target, Errors errors) { // target으로 검증할 객체가 넘어온다. Errors는 BindingResult의 부모클래스이다.
        Item item = (Item) target; // Item 으로 캐스팅해서 사용


        //컨트롤러에서 진행하던 검증부분을 그대로 가져왔다.
        // Errors는 BindingResult의 부모클래스이므로 Errors에 BindingResult를 저장할 수있고
        // errors안에도 reject,rejectValue가 존재하므로 그대로 메소드를 사용할 수 있다.
        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() >= 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);
            }
        }

    }
}

Errors는 BindingResult의 부모클래스이다.

 

 

itemAdd 메소드 V5 

 

1. 스프링빈으로 등록되어있는 ItemValidator를 의존관계 주입한다. (생성자 주입)

2. 메소드 코드 내용

@PostMapping("/add")
public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
    
    itemValidator.validate(item, bindingResult); // validator 를 통해 검증을 진행한다. (bindingResult 객체를 전달해서 검증실패시 그 안에 에러들이 담기게한다)


    if (bindingResult.hasErrors()) {
        log.info("erros = {}", 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}";
}

 

원래는 아래코드처럼 class타입을 먼저 확인하고 검증메소드를 호출해야하긴하지만 바로 검증메소드를 호출해주었다.

if (itemValidator.supports(item.getClass())) { // validator 의 supports()를 이용해서 validator 에서 처리는 class가 맞는지 체크하고
    itemValidator.validate(item, bindingResult); // validator 를 통해 검증을 진행한다. (bindingResult 객체를 전달해서 검증실패시 그 안에 에러들이 담기게한다)
}

 

실행해보면 기존과 완전히 동일하게 동작하는 것을 확인할 수 있다.

검증과 관련된 부분이 깔끔하게 분리되었다.

 

하지만 사실 스프링빈으로 등록하지않고(@Component 쓰지않고) Validator를 implement해서 

사용하지도않는 supports()를 구현하지않고

 

그냥 validat()메소드를 만들어서

new ItemValidator()로 객체생성해서 가져온후 검증메소드를 사용해도 동작하긴한다.

(하지만 사용하는곳에서 매번 객체를 생성해줘야한다는 단점도 있긴함)

 

그렇다면 왜 굳이 Validator 인터페이스를 구현해서 사용하는것인가는 

 

밑에서 알게된다.


Validator 분리2

스프링이 Validator 인터페이스를 별도로 제공하는 이유 체계적으로 검증 기능을 도입하기 위해서다.

 

그런데 앞에서는 검증기를 직접 불러서 사용했고, 이렇게 사용해도 된다. ( Validator 인터페이스를 구현하고,

스프링빈으로 등록해서 주입받아 검증메소드를 사용하는 방식)

 

그런데 Validator 인터페이스를 사용해서 검증기를 만들면 스프링의 추가적인 도움을 받을 수 있다.

 

 

 

WebDataBinder를 통해서 사용하기

WebDataBinder 스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다

(@ModelAttribute를 붙이면 알아서 객체 바인딩해주는 그 파라미터 바인딩 기능과

validator를 넣어주면 검증기능 까지해주는 WebDataBinder를 스프링이 사용한다.)

 

WebDataBinder 코드 추가

@Slf4j
@Controller
@RequestMapping("/validation/v2/items")
@RequiredArgsConstructor
public class ValidationItemControllerV2 {

    private final ItemRepository itemRepository;
    private final ItemValidator itemValidator;

    @InitBinder
    public void init(WebDataBinder dataBinder) {
        dataBinder.addValidators(itemValidator);
    }

이렇게 WebDataBinder 에 검증기(Validator)를 추가하면

해당 컨트롤러에서는 검증기(Validator)자동으로 적용할 수 있다.

@InitBinder가 있는 해당 컨트롤러에만 영향을 준다. 글로벌 설정은 별도로 해야한다. (글로벌 설정은 마지막에 설명)

 

 

addItem 메소드 V6

@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {


    if (bindingResult.hasErrors()) {
        log.info("erros = {}", 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}";
}

@Validated라는 어노테이션을 추가되고 , 검증기의 검증메소드호출부분을 지웠다.

@Validated @ModelAttribute Item item

 

@Validated 동작방식

 

@Validated 는 검증기를 실행하라는 애노테이션이다.

 

이 애노테이션이 붙으면 앞서 WebDataBinder 에 등록한 검증기를 찾아서 실행한다.

그런데 여러 검증기를 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다. 이때 supports() 가 사용된다.

여기서는 supports(Item.class) 호출되고, 결과가 true 이므로 ItemValidator 의 validate()가 호출된다.

validate()가 호출되면서 생기는 에러(필드에러,오브젝트에러)들은 알아서 BindingResult에 담긴다.

 

 

 

 

글로벌 설정 - 모든 컨트롤러에 다 적용 (참고용)

컨트롤러마다 @InitBinder를 이용해서 WebDataBinder에 Validator(검증기)를 등록하지않고 

 

서버실행에 사용하는 application파일로 가서 WebMvcConfigurer를 implements하고 다음 코드를 추가하면 

알아서 WebDataBinder에 Validator(검증기)가 등록된다.

@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {

   public static void main(String[] args) {
      SpringApplication.run(ItemServiceApplication.class, args);
   }

   @Override
   public Validator getValidator() {
      return new ItemValidator();
   }

}

 

 

글로벌 설정을 하면 다음에 설명할 BeanValidator가 자동 등록되지 않는다. 글로벌 설정 부분은 주석처리 해두자.

참고로 글로벌 설정을 직접 사용하는 경우는 드물다!

 

 

@Validated , @Valid

 

 

@Validated @Valid 둘다 같은 애노테이션입니다.

@Valid: 자바 진영에서 공통으로 사용 가능

@Validated: 스프링 안에서만 사용가능

 

@Validated@Valid로 바꿔도 잘동작한다.

 

자세한건 다음 Bean Validation에

댓글