ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [김영한 스프링] 25. 검증1 Validation - Validator 분리1, 2
    Spring/스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 2023. 9. 2. 02:13

    Validator 분리1

     

    목표

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

     

    컨트롤러에서 검증 로직이 차지하는 부분은 매우 크다. 이런 경우 별도의 클래스로 역할을 분리하는 것이 좋다. 그리고 이렇게 분리한 검증 로직을 재사용할 수도 있다.

     

     

    ItemValidator

    main/java/hello/itemservice/web/validation/ItemValidator 생성

     

     

    package hello.itemservice.web.validation;
    
    import hello.itemservice.domain.item.Item;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    import org.springframework.validation.Errors;
    import org.springframework.validation.Validator;
    
    @Component
    public class ItemValidator implements Validator {
    
        @Override
        public boolean supports(Class<?> clazz) {
            return Item.class.isAssignableFrom(clazz);
            // isAssignableFrom
            // item == clazz와 같은 의미
            // item == subItem 자식도 검증 가능
    
        }
    
        @Override
        public void validate(Object target, Errors errors) {
            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() >= 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);
                }
            }
        }
    }

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

    public interface Validator {
        boolean supports(Class<?> clazz);
        void validate(Object target, Errors errors);
    }
    • supports() {} : 해당 검증기를 지원하는 여부 확인(뒤에서 설명)
    • validate(Object target, Errors errors) : 검증 대상 객체와 BindingResult

     

     

    ItemValidator 직접 호출하기 - ValidationItemControllerV2

        private final ItemValidator itemValidator;
    
        @PostMapping("/add")
        public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
    
            itemValidator.validate(item, bindingResult);
    
            // 검증에 실패하면 다시 입력 폼으로
            if (bindingResult.hasErrors()) {
                log.info("error = {} ", 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}";
        }

    코드 변경

    • addItemV4()의 @PostMapping 부분 주석 처리

     

    ItemValidator를 스프링 빈으로 주입받아서 직접 호출했다. 

     

     

    Validator 분리2

     

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

     

    WebDataBinder를 통해서 사용하기

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

     

     

    ValidationItemControllerV2

        @InitBinder
        public void init(WebDataBinder dataBinder) {
            dataBinder.addValidators(itemValidator);
        }
    
        @PostMapping("/add")
        public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
    
    //        itemValidator.validate(item, bindingResult);
    
            // 검증에 실패하면 다시 입력 폼으로
            if (bindingResult.hasErrors()) {
                log.info("error = {} ", 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}";
        }

    items가 호출되던지, item이 호출되던지, addItemV6가 호출되던지 init가 먼저 실행됨

     

    WebDataBinder에 검증기를 추가하면 해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있다.

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

     

    코드 변경

    • addItemV5()의@PostMapping 부분 주석 처리

     

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

     

    동작 방식

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

    이 애노테이션이 붙으면 앞서 WebDataBinder에 등록한 검증기를 찾아서 실행한다. 그런데 여러 검증기를 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다. 이때 supports()가 사용된다. 여기서는 supports(Item.class) 호출되고, 결과가 true이므로 ItemValidator의 validate()가 호출된다.

     

     

    실행

     

     

    참고

    검증 시 @Validated, @Valid 둘 다 사용가능하다.
    javax.validation.@Valid를 사용하려면 build.gradle 의존관계 추가가 필요하다.
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    @Validated는 스프링 전용 검증 애노테이션이고, @Valid는 자바 표준 검증 애노테이션이다.
    자세한 내용은 다음 Bean Validation에서 설명하겠다.

     

     

    출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2

     

    스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

    웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

    www.inflearn.com

Designed by Tistory.