bindingResult.addError(new FieldError("item", "itemName", "사용자입력값", false, null, null, "required item name"));
보면 field 에러에서 순서대로
objectName : 오류가 발생한 객체 이름
field : 오류 필드
rejectedValue : 사용자가 입력한 값(거절된 값)
bindingFailure : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값
codes : 메시지 코드
arguments : 메시지에서 사용하는 인자
defaultMessage : 기본 오류 메시지
라고 하였다.
이때 5, 6번째 argument가 바로 우리가 처리해야할 오류 코드와 오류 메시지이다.
그럼 일단 앞저번 처럼 message를 등록해보자.
resources
폴더 바로 밑에 errors.properites
파일을 만들어 작성해준다.required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
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}
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));
}
}
target
에 대해 알고있는 상태이다.void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String 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
처럼 디테일하게 적게될 경우엔 객체의 개수만큼 적어야해서 비효율적이다.
따라서 아래처럼 전반전이고 단순한 것을 하나 만들어두고 필요에 따라서 디테일 한 것을 추가하여 잡으면 될 것이다.
required : 필수 값 입니다.
range : 범위 오류 입니다.
#Level1
required.item.itemName: 상품 이름은 필수 입니다.
#Level2
required: 필수 값 입니다.
이렇게 생겼다면 위에게 먼저 적용된다는 것이다.
이때 효율적인 개발 방법은 어차피 properties가 없더라고 nullPointException은 안 나니까 미리 만들어서 넣어두고
new Object[]{"required.item.itemName","required"}
처럼
나중에 해당 properties 파일만 조작하면 개발 코드를 따로 건들필요가 없어진다.
그런데 이게 가능하게 해주는 것이 바로 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");
}
}
보면 디테일 한것 부터 순서대로 나오는 것을 확인할 수 있다.
동작 방식
rejectValue()
, reject()
는 내부에서 MessageCodesResolver
를 사용한다.FieldError
, ObjectError
의 생성자를 보면, 오류 코드를 하나가 아니라 여러 오류 코드(new Object[]{}
)를 가질 수 있다.MessageCodesResolver
를 통해서 생성된 순서대로 오류 코드를 보관한다.ObjectError
ObjectError reject("totalPriceMin")
일때 2가지 오류 코드를 자동으로 생성.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 에서 메시지 코드들로 메시지를 순서대로 메시지에서 찾고, 노출
#==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.