Spring Validation 2(errorcode and message processing)

강정우·2023년 12월 14일
0

Spring-boot

목록 보기
41/73

오류코드와 메시지 처리

  • 앞서 우리가 유효성 검사를 하고 에러를 보면 굉장히 무섭게 생겼다. 그래서 사람 친화적으로 이 메시지를 바꿔보자.

FieldError

codes, arguments

bindingResult.addError(new FieldError("item", "itemName", "사용자입력값", false, null, null, "required item name"));
  • 보면 field 에러에서 순서대로
    objectName : 오류가 발생한 객체 이름
    field : 오류 필드
    rejectedValue : 사용자가 입력한 값(거절된 값)
    bindingFailure : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값
    codes : 메시지 코드
    arguments : 메시지에서 사용하는 인자
    defaultMessage : 기본 오류 메시지
    라고 하였다.

  • 이때 5, 6번째 argument가 바로 우리가 처리해야할 오류 코드와 오류 메시지이다.
    그럼 일단 앞저번 처럼 message를 등록해보자.

에러 메시지 properties 작성 및 등록

  • 우선 resources 폴더 바로 밑에 errors.properites 파일을 만들어 작성해준다.

errors.properties

required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}

errors_en.properties

required.item.itemName=u required valid item name.
range.item.price=price allowed {0} to {1}.
max.item.quantity=max quantity is {0}.
totalPriceMin=price times by quantity must be at least {0}. current price = {1}
  • 이런식으로 국제화를 위해 en파일도 추가적으로 작성해주고 application.properties 파일에 errors도 등록해준다.
spring.messages.basename=messages, errors
  • 그리고 단축키 cmd + e 를 누르면 최근열어본 파일이 나오는데 이걸로 편리하게 등록했던 파일을 왔다갔다하면 된다.
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() > 1_000_000) {
    bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, new String[]{"range.item.price"}, new Object[]{1_000, 1_000_000}, null));
}
if (item.getQuantity() == null || item.getQuantity() >= 999) {
    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 < 10_000) {
        bindingResult.addError(new ObjectError("item", new String[]{"totalPriceMin"}, new Object[]{10_000, resultPrice}, "The item quantity multiplied by the item price must exceed 10 dollars. current prices = " + resultPrice));
    }
}

new Object[]{}로 "배열"이 들어가는 이유

  • 만약 해당 property를 못 찾을 경우 다음 property가 자동으로 들어간다.
    그런데 또 없다면 default message가 들어가고 만약 이 마저도 없다면 white label error page가 나온다.

BindingResult

  • 앞서 우리는 이 BindingResult 객체가 항상 @ModelAttribute 다음에 온다는 것을 알고 있다.
    그래서 사실 이 BindingResult는 이미 검증대상인 target에 대해 알고있는 상태이다.
    이를 이용하여 더 간편하게 오류메시지를 처리할 수 있다.

rejectValue() , reject()

  • BindingResult 가 제공하는 rejectValue() , reject() 를 사용하면 FieldError , ObjectError 를 직접 생성하지 않고, 깔끔하게 검증 오류를 다룰 수 있다.
void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage); 
  • field : 오류 필드명
  • errorCode : 오류 코드(이 오류 코드는 메시지에 등록된 코드가 아니다. 뒤에서 설명할 messageResolver를 위한 오류 코드이다.)
  • errorArgs : 오류 메시지에서 {0} 을 치환하기 위한 값
  • defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지
if (!StringUtils.hasText(item.getItemName())) {
    bindingResult.rejectValue("itemName", "required");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1_000_000) {
    bindingResult.rejectValue("price", "range", new Object[]{1000, 1_000_000}, null);
}
if (item.getQuantity() == null || item.getQuantity() >= 999) {
    bindingResult.rejectValue("quantity", "max", new Object[]{9999}, null);
}
if (item.getPrice() != null && item.getQuantity() != null) {
    int resultPrice = item.getPrice() * item.getQuantity();
    if (resultPrice < 10_000) {
        bindingResult.reject("totalPriceMin", new Object[]{10_000, resultPrice}, null);
    }
}
  • 사실 addError에 new FieldError, ObjectError 이런거를 rejectValue 메서드가 안에서 다 해주는 것이다.

  • 그래서 bindingResult 객체가 사실 item에 해당하는 값을 다 알고 있기에 위 코드처럼 간결한 코드가 가능한 것이다.
    또한 errors.properties의 패턴을 잘 보면 사용자에러코드.객체명.필드명 패턴으로 프로퍼티 파일을 작성한 것을 알 수 있다.

  • 하지만 여기서 우리가 생각해 봐야할 점이 모든 메시지를 errors.properties 처럼 디테일하게 적게될 경우엔 객체의 개수만큼 적어야해서 비효율적이다.
    따라서 아래처럼 전반전이고 단순한 것을 하나 만들어두고 필요에 따라서 디테일 한 것을 추가하여 잡으면 될 것이다.

general.properties

required : 필수 값 입니다.
range : 범위 오류 입니다.
  • 그리고 역시 모든 코드는 디테일 한 것은 범용적인것을 항상 이긴다.
#Level1
required.item.itemName: 상품 이름은 필수 입니다.
#Level2
required: 필수 값 입니다.
  • 이렇게 생겼다면 위에게 먼저 적용된다는 것이다.

    이때 효율적인 개발 방법은 어차피 properties가 없더라고 nullPointException은 안 나니까 미리 만들어서 넣어두고 new Object[]{"required.item.itemName","required"} 처럼
    나중에 해당 properties 파일만 조작하면 개발 코드를 따로 건들필요가 없어진다.

  • 그런데 이게 가능하게 해주는 것이 바로 MessageCodesResolver 이다.

MessageCodesResolver

  • 검증 오류 코드로 메시지 코드들을 생성한다.
  • MessageCodesResolver 인터페이스이고 DefaultMessageCodesResolver 는 기본 구현체이다.
  • 주로 ObjectError , FieldError과 함께 사용한다.
public class MessageCodesResolverTest {
    MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();

    @Test
    void messageCodesResolverObject() {
        String[] messageCodes = codesResolver.resolveMessageCodes("required", "item");
        for (String messageCode : messageCodes) {
            System.out.println("messageCode = " + messageCode);
        }
        assertThat(messageCodes).containsExactly("required.item", "required");
    }

    @Test
    void messageCodesResolverField() {
        String[] messageCodes = codesResolver.resolveMessageCodes("required", "item", "itemName", String.class);
        for (String messageCode : messageCodes) {
            System.out.println("messageCode = " + messageCode);
        }
        assertThat(messageCodes).containsExactly("required.item.itemName", "required.itemName", "required.java.lang.String", "required");
    }
}

  • 보면 디테일 한것 부터 순서대로 나오는 것을 확인할 수 있다.

  • 동작 방식

  1. rejectValue() , reject() 는 내부에서 MessageCodesResolver 를 사용한다.
    여기에서 메시지 코드들을 생성한다.
    FieldError , ObjectError 의 생성자를 보면, 오류 코드를 하나가 아니라 여러 오류 코드(new Object[]{})를 가질 수 있다.
  2. MessageCodesResolver 를 통해서 생성된 순서대로 오류 코드를 보관한다.
  3. 오류 메시지 출력
    타임리프 화면을 렌더링 할 때 th:errors 가 실행된다. 만약 이때 오류가 있다면 생성된 오류 메시지 코드를 순서대로 돌아가면서 메시지를 찾는다. 그리고 없으면 디폴트 메시지를 출력한다.
  • 참고 ObjectError
    ObjectError reject("totalPriceMin") 일때 2가지 오류 코드를 자동으로 생성.
    1.totalPriceMin.item
    2.totalPriceMin

ValidationUtils

ValidationUtils 사용 전

if (!StringUtils.hasText(item.getItemName())) {
 bindingResult.rejectValue("itemName", "required", "기본: 상품 이름은 필수입니다.");
}

ValidationUtils 사용 후

ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName","required");
  • 다음과 같이 한줄로 가능하다. 어떻게 이게 되냐면 안에 들어가보면 해당 메서드가 !StringUtils.hasText(item.getItemName())를 구현하기 때문이다.

정리
1. rejectValue() 호출
2. MessageCodesResolver 를 사용해서 검증 오류 코드로 메시지 코드들을 생성
3. new FieldError() 를 생성하면서 메시지 코드들을 보관
4. th:erros 에서 메시지 코드들로 메시지를 순서대로 메시지에서 찾고, 노출

spring error message 사용해보기

  • 보면 Inteager 값에 String을 넣었다고 가정해보자.
    해당 form은 item 이라는 객체로 바인딩 되어있고 price는 Inteager로 설정되어있어 String이 들어가면 item 객체 바인딩이 불가능하다.
    그래서 보면 spring이
    typeMismatch.item.price
    typeMismatch.price
    typeMismatch.java.lang.Integer
    typeMismatch
    이렇게 4개의 FieldError를 만든것을 확인할 수 있다.

  • 그리고 해당 메시지가 바로 저 매우 개발자 스러운 메시지인 것이다.
    그래서 우리는 spring이 작성해둔 defaultMessage를 properties에 덮어씌우면 되는 것이다.

error.properties

#==ObjectError==
#Level1
totalPriceMin.item=Price times by quantity must be at least {0}. current price = {1}
#Level2 - 생략
totalPriceMin=Total price must be over {0}. current price = {1}

#==FieldError==
#Level1
required.item.itemName=Product name required.
range.item.price=Price allowed between {0} and {1}.
max.item.quantity=max quantity is {0}.
#Level2 - 생략
#Level3
required.java.lang.String = Required string.
required.java.lang.Integer = Required Number.
min.java.lang.String = u need over {0} words.
min.java.lang.Integer = u need more than {0}.
range.java.lang.String = u can type between {0} and {1} words.
range.java.lang.Integer = u can type between {0} and {1} numbers.
max.java.lang.String = max length is {0}.

#==TypeError==
#Level1
typeMismatch.item.price = Not valid price value (item)
#Level2
typeMismatch.price = Not valid price value
#Level3
typeMismatch.java.lang.Integer = It must be number type
#Level4
typeMismatch = Type is not right.

  • 그럼 이제 사람냄새 나는 에러메시지를 볼 수 있다.
profile
智(지)! 德(덕)! 體(체)!

0개의 댓글