Spring/Spring MVC

Spring MVC - 검증(Validation)에 errors MessageSource 사용

jddng 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
반응형