ABOUT ME

Today
Yesterday
Total
  • [스프링MVC - 2편] 검증 (2)
    스프링&스프링부트 2025. 2. 9. 19:27

    오류 코드와 메시지 처리 1

    errors 메시지 파일 생성

    errors.properties라는 별도의 일로 관리한다

    FieldError, ObjectError 생성자는 codes, arguments 제공하며 이것은 오류 발생시 오류 코드로 시지를 찾기 위해 사용된다

     

     

    스프링 부트 메시지 설정 추가

    // application.properties
    spring.messages.basename=messages,errors
    
    // src/main/resources/errors.properties
    required.item.itemName=상품 이름은 필수입니다.
    range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
    max.item.quantity=수량은 최대 {0} 까지 허용합니다.
    totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}

     

     

    2-4. ValidationItemControllerV2 - addItemV3() 추가

    @PostMapping("/add")
    public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
        // 검증 로직
        if (!StringUtils.hasText(item.getItemName())) {
           bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[]{"required.item.itemName"}, null, null));
        }
    
        if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000){
            bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, new String[]{"range.item.price"}, new Object[]{1000, 1000000}, null));
        }
    
        if (item.getQuantity() == null || item.getQuantity() >= 9999) {
            bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false, new String[]{"max.item.quantity"}, new Object[]{9999}, null));
        }
    
        // 특정 필드가 아닌 복합 룰 검증
        if (item.getPrice() != null && item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice < 10000) {
                bindingResult.addError(new ObjectError("item", new String[]{"totalPriceMin"}, new Object[]{10000, resultPrice}, null));
            }
        }
    
        // 검증에 실패하면 다시 입력 폼으로 이동
        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}";
    }

     

     

     

    오류 코드와 메시지 처리 2

    컨트롤러에서 BindingResult 검증해야 객체인 target 바로 다음에 온다

    따라서 BindingResult 본인이 검증해야 객체인 target 알고 있다

    log.info("objectName={}", bindingResult.getObjectName());
    log.info("target={}", bindingResult.getTarget());
    
    // 출력 결과
    // objectName=item //@ModelAttribute name
    // target=Item(id=null, itemName=상품, price=100, quantity=1234)

     

     

    2-5. ValidationItemControllerV2 - addItemV4() 추가

    // 검증 로직
    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);
        }
    }

     

     

    rejectValue()

    void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, 
                     @Nullable String defaultMessage);
    • field : 오류 필드명
    • errorCode : 오류 코드 (messageResolver)
    • errorArgs : 오류 메시지에서 {0}을 치환하기 위한 
    • defaultMessage : 오류 메시지를 찾을  없을  사용하는 기본 메시지

     

    reject()

    void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);

     

     

     

    오류 코드와 메시지 처리 3

     

     

     

    오류 코드와 메시지 처리 4

    MessageCodesResolverTest

    public class MessageCodesResolverTest {
        MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
    
        @Test
        void messageCodesResolverObject() {
            String[] messageCodes = codesResolver.resolveMessageCodes("required", "item");
            assertThat(messageCodes).containsExactly("required.item", "required");
        }
        
        @Test
        void messageCodesResolverField() {
            String[] messageCodes = codesResolver.resolveMessageCodes("required", "item", "itemName", String.class);
            assertThat(messageCodes).containsExactly(
                                          "required.item.itemName",
                                          "required.itemName",
                                          "required.java.lang.String",
                                          "required"
            );
        }
    }

    MessageCodesResolver

    • 검증 오류 코드로 메시지 코드들을 생성한다
    • MessageCodesResolver 인터페이스이고 DefaultMessageCodesResolver는 기본 구현체이다
    • 주로 ObjectError, FieldError 함께 사용 

     

     

    DefaultMessageCodesResolver 기본 메시지 생성 규칙

    1. 객체 오류

    1. : code + "." + object name

    2. : code

    ) 오류 코드: required, object name: item

    1. : required.item

    2. : required

     

    2. 필드 오류

    1. : code + "." + object name + "." + field

    2. : code + "." + field

    3. : code + "." + field type

    4. : code

    ) 오류 코드: typeMismatch, object name "user", field "age", field type: int

    1. "typeMismatch.user.age"

    2. "typeMismatch.age"

    3. "typeMismatch.int"4. "typeMismatch"

     

     

    동작 방식

    • rejectValue(), reject()는 내부에서 MessageCodesResolver를 사용하고 여기에서 메시지 코드들을 생성한다
    • FieldError, ObjectError의 생성자를 보면, 오류 코드를 하나가 아니라 여러 오류 코드를 가질  있다
    • MessageCodesResolver를 통해서 생성된 순서대로 오류 코드를 보관한다
    • BindingResult의 로그 : codes [range.item.price, range.price, range.java.lang.Integer, range]

     

     

    FieldError - rejectValue("itemName", "required")

    다음 4가지 오류 코드를 자동으로 생성

    • required.item.itemName
    • required.itemName
    • required.java.lang.String
    • required

     

    ObjectError - reject("totalPriceMin")

    다음 2가지 오류 코드를 자동으로 생성

    • totalPriceMin.item
    • totalPriceMin

     

     

     

    오류 코드와 메시지 처리 5

    ValidationUtils

    // ValidationUtils 사용 전
    if (!StringUtils.hasText(item.getItemName())) {
        bindingResult.rejectValue("itemName", "required","기본: 상품 이름은 필수입니다.");
    }
    
    // ValidationUtils 사용 후
    // 다음과 같이 한줄로 가능, 제공하는 기능은 Empty, 공백 같은 단순한 기능만 제공
    ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");

     

     

    정리

    1. rejectValue() 호출

    2. MessageCodesResolver를 사용해서 검증 오류 코드로 메시지 코드들을 생성

    3. new FieldError() 생성하면서 메시지 코드들을 보관

    4. th:erros에서 메시지 코드들로 메시지를 순서대로 메시지에서 찾고, 노출

     

     

    오류 코드와 메시지 처리 6

     

     

     

    Validator 분리1

    복잡한 검증 로직을 별도로 분리한다

     

    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;
    
            // 검증 로직
            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);
                }
            }
        }
    }
    • supports() {} : 해당 검증기를 지원하는 여부 확인
    • validate(Object target, Errors errors) : 검증 대상 객체와 BindingResult

     

     

    ValidationItemControllerV2 - addItemV5()

    @PostMapping("/add")
    public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
    
        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}";
    }

     

     

     

     

    Validator 분리2

    WebDataBinder 사용

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

     

     

    ValidationItemControllerV2

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

     

     

    ValidationItemControllerV2 - addItemV6()

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

    동작 방식

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

    애노테이션이 붙으면 앞서 WebDataBinder 등록한 검증기를 찾아서 실행한다 그런데 여러 검증기를 등록한다면 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다 

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

     

     

     

     

     

    728x90
Designed by Tistory.