ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring MVC - 검증(Validation)에 errors MessageSource 사용
    Spring/Spring MVC 2022. 2. 6. 16:25
    728x90
    반응형

     

     

     

     

     

    검증(Validation)에 errors MessageSource 사용

     검증에 위반할 때 오류 코드에 대한 defaultMessage를 매번 적어서 사용하는 것보단 메시지 파일을 만들어 체계적으로 다루는 방법이 효율적이다. errors.properties 메시지 파일을 만들어 어떻게 이용하는지 점진적으로 하나씩 알아보자. 

     

     

     

     

    MessageSource 설정 추가

     

    • MessageSource가 자동으로 errors.properties 파일을 읽어오기 위한 설정을 추가해야 한다.
    • application.properties
      spring.messages.basename=messages,errors​
      MessageSource는 기본으로 messages.properties만 읽어온다. 따라서 errors.properties
         읽어올 수 있도록 설정에 errors 도 추가해줘야 한다.

     

    errors 메시지 파일 생성

     

    • 에러 메시지를 관리하기 위한 파일
    • 에러 메시지도 국제화 처리를 할 수 있다.
    • errors.properties
      required.item.itemName=상품 이름은 필수입니다.
      range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
      max.item.quantity=수량은 최대 {0} 까지 허용합니다.
      totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
      ▶ 예제 코드에 사용할 errorCode와 error message 작성

     

    FieldError, ObjectError를 이용한 에러 메시지 파일 사용

     

     FieldError, ObjectErrorerrorCode로 해당 에러 메시지를 가져올 수 있도록 구현되어 있다. 아래 각각의 생성자를 살펴보자. 

     

    FieldError 의 생성자

    public FieldError(String objectName, String field, 
                      @Nullable Object rejectedValue, boolean bindingFailure, 
                      @Nullable String[] codes, @NullableObject[] arguments, 
                      @Nullable String defaultMessage) {...}
    • objectName    : 오류가 발생항 객체 이름
    • field              : 오류 필드
    • rejectedValue  : 사용자가 입력한 값(거절된 값)
    • bindingFailure : 타입 오류 같은 바이딩 실패인지, 검증 실패인지 구분 값
    • codes            : 메시지 코드
    • arguments      : 메시지에서 사용하는 인자
    • defaultMessage : 기본 오류 메시지

     

    ObjectError 의 생성자

    public ObjectError(
            String objectName, @Nullable String[] codes, 
            @Nullable Object[] arguments,
            @Nullable String defaultMessage) {...}
    • objectName    : 오류가 발생항 객체 이름
    • codes            : 메시지 코드
    • arguments      : 메시지에서 사용하는 인자
    • defaultMessage : 기본 오류 메시지

     

     

     FieldError, ObjectError 클래스를 사용하여 원하는 errorCode로 errors.properties 파일에서 해당 error message를 가져올 수 있다.

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

    codes : required.item.itemName 를 사용하여 errorCode 지정
                  errorCode는 배열로 여러 값을 전달할 수 있는데 순서대로 매칭 하여 처음 매칭 되는 메시지를 사용
    arguments : Objec 배열을 사용하여 메시지에 전달 인자를 전달하여 치환됨
                        ( {0}, {1}    →    {1000, 1000000} )
                        ( {0}, {1}    →    {10000, resultPrice} )

     

     

    더보기

    전체 코드

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

     


     

     

    rejectValue(), reject()로 에러 메시지 파일 사용

     위 방법보다 BindingResult가 제공하는 rejectValue(), reject() 를 사용하면 FieldError, ObjectError를 직접 생성하지 않고 깔끔하게 검증 오류를 다룰 수 있다. 

     

     FieldError, ObjectError를 직접 생성하지 않고 BindingResult가 제공하는 rejectValue(), reject() 를 사용할 수 있는 이유는 스프링이 처리해주기 때문이다. BindingResult는 검증해야 할 객체(target) 바로 다음에 온다. 따라서 BindingResult는 이미 본인이 검증해야 할 객체인 target을 컨트롤러가 호출될 때 이미 알고 있는 상태이다.

     ex)

    public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model)

     

     BindingResult가 검증 객체를 알고 있는지 logging을 이용하여 확인해보자.

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

     위 결과와 같이 BindingResult는 컨트롤러가 호출되는 시점에 이미 검증 객체(objectName)을 이미 알고 있다는 것을 확인할 수 있다.

     

     

    rejectValue(), reject()

     

    • rejectValue(), reject() 메서드는 내부적으로 FieldError, ObjectError를 생성해준다.
    • 이미 objectName을 알고 있기 때문에 코드 작성이 용이하다.
    • 메서드 정보
      void rejectValue(@Nullable String field, String errorCode);
      void rejectValue(@Nullable String field, String errorCode, String defaultMessage);
      void rejectValue(@Nullable String field, String errorCode,
      		 @Nullable Object[] errorArgs, @Nullable String defaultMessage);
                  
      void reject(String errorCode);
      void reject(String errorCode, String defaultMessage);
      void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);​
      • field : 오류 필드명
      • errorCode : 오류 코드
      • errorArgs : 오류 메시지에 전달할 전달 인자
      • defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지

     

     

     rejectValue(), reject() 메서드를 사용하여 원하는 errorCode로 errors.properties 파일에서 해당 error message를 가져올 수 있다.

    //bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, null, null, "상품 이름은 필수 입니다."));
    bindingResult.rejectValue("itemName", "required");
    
    //bindingResult.addError(new ObjectError("item",new String[]{"totalPriceMin"} ,new Object[]{10000, resultPrice}, null));
    bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);

     

     참고로 ValidationUtils은 내부적으로 if 조건에 대한 처리가 메서드로 구현되어있어 사용하기 편리하다.

    if (!StringUtils.hasText(item.getItemName())) {
        bindingResult.rejectValue("itemName", "required");
    }
    
    //위 코드와 동일
    ValidationUtils.rejectIfEmptyOrwhitespace(bindingResult, "itemName", "required");

     

     

     

    더보기

    전체 코드

    @PostMapping("/add")
    public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
    
        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, 10000000}, 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);
            }
        }
    
        //검증에 실패하면 다시 입력 폼으로
        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}";
    }

     

    MessageCodesResolver

     

    • rejectValue, reject 내부에서 errorCode로 메시지 코드들을 생성해주는 인터페이스
    • DefaultMessageCodesResolver를 구현체로 사용한다.
    • 생성한 메시지 코드들을 FieldError, ObjectError의 매개변수에 String[] 타입으로 인자를 전달하여 생성
      (FieldError, ObjectError의 매개변수 String[] codes 에 생성된 메시지 코드들을 전달)
    • rejectValue("필드명", "에러코드") 에서 생성되는 메시지 코드
      우선순위 메시지 코드 생성 방법 예시( rejectValue("itemName", "required") )
      1  code + "." + object name + "." + field  required.item.itemName
      2  code + "." + field  required.itemName
      3  code + "." + field type  required.java.lang.String
      4  code  required
    • reject("에러코드") 에서 생성되는 메시지 코드
      우선순위 메시지 코드 생성 방법 예시( reject("totalPriceMin") )
      1  code + "." + object name  totalPriceMin.item
      2  code  totlaPriceMin
    • 타임리프 화면을 렌더링 할 때 th:errors가 오류가 있다면
      오류 메시지 코드로 우선순위 순서대로 해당 메시지를 찾는다. 

     

    errors.properties 예시

    #==ObjectError==
    #우선순위 1
    totalPriceMin.item=상품의 가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
    
    #우선순위 2
    totalPriceMin=전체 가격은 {0}원 이상이어야 합니다. 현재 값 = {1}
    
    
    #==FieldError==
    #우선순위 1
    required.item.itemName=상품 이름은 필수입니다.
    range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
    max.item.quantity=수량은 최대 {0} 까지 허용합니다.
    
    #우선순위 2 - 생략
    
    #우선순위 3
    required.java.lang.String = 필수 문자입니다.
    required.java.lang.Integer = 필수 숫자입니다.
    min.java.lang.String = {0} 이상의 문자를 입력해주세요.
    min.java.lang.Integer = {0} 이상의 숫자를 입력해주세요.
    range.java.lang.String = {0} ~ {1} 까지의 문자를 입력해주세요.
    range.java.lang.Integer = {0} ~ {1} 까지의 숫자를 입력해주세요.
    max.java.lang.String = {0} 까지의 숫자를 허용합니다.
    max.java.lang.Integer = {0} 까지의 숫자를 허용합니다.
    
    #우선순위 4
    required = 필수 값 입니다.
    min= {0} 이상이어야 합니다.
    range= {0} ~ {1} 범위를 허용합니다.
    max= {0} 까지 허용합니다.
    
    #스프링에서 자동으로 생성하는 오류 코드에 대한오류 메시지 또한 사용자 정의로 만들 수 있다.
    typeMismatch.java.lang.Integer=숫자를 입력해주세요.
    typeMismatch=타입 오류입니다.

     


     

    728x90
    반응형

    댓글

Designed by Tistory.