required : 필수 값이다.
range : 범위 오류이다.
→ 비슷한 에러가 여러 개 발생하면 어디에서 발생한 에러인지 바로 파악하기 어렵다.
required.item.itemName: 상품 이름은 필수이다.
range.item.price`: 상품의 가격 범위 오류이다.
→ 너무 자세하기 때문에 모든 경우마다 만들어줘야해서 비효율적이다.
가장 좋은 방법은 범용성의 수준에 따라 메시지에 단계를 두는 것이다!
범용성으로 사용하다가, 세밀하게 작성해야 하는 경우에는 세밀한 내용이 적용되도록 하자!
예를 들어 required
오류가 발생할 때,
#Level1
required.item.itemName: 상품 이름은 필수이다.
#Level2
required: 필수 값이다.
평소에는 required
가 출력되도록 하다가, 특별하게 itemName
값이 채워지지 않은 경우에는
더 구체적인 required.item.itemName
이 출력되도록 하는 것이다!
스프링은 MessageCodesResolver
을 통해 이러한 기능을 제공한다.
본격적으로 우리 프로젝트에 적용하기 전에,
테스트 코드를 통해 MessageCodesResolver
에 대해 알아보자!
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 messageCodeResolverField() {
//필드 오류
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item", "itemName", String.class);
assertThat(messageCodes).containsExactly(
"required.item.itemName",
"required.itemName",
"required.java.lang.String",
"required"
);
}
}
MessageCodesResolver
MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
MessageCodesResolver
는 인터페이스, DefaultMessageCodesResolver
는 기본 구현체ObjectError
, FieldError
와 함께 사용한다.DefaultMessageCodesResolver
의 기본 메시지 생성 규칙ObjectError
객체 오류의 경우 다음 순서로 2가지 생성
1.: code + "." + object name
2.: code
예) 오류 코드: required, object name: item
1.: required.item
2.: required
필드 오류의 경우 다음 순서로 4가지 메시지 코드 생성
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"
💡 구체적인 것에서 덜 구체적인 것으로!
MessageCodesResolver
는 구체적인 것을 먼저, 덜 구체적인 것을 나중에 만든다.
→ 이렇게 하면 메시지와 관련된 공통 전략을 편리하게 도입할 수 있다!
모든 오류 코드에 대해서 메시지를 각각 다 정의하면 너무 비효율적이기 때문에,
requried
같은 메시지로 끝내고, 하는 방식이 더 효과적이다!
이제 우리 프로젝트에 적용해보자!
errors.properties
#required.item.itemName=상품 이름은 필수입니다.
#range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
#max.item.quantity=수량은 최대 {0} 까지 허용합니다.
#totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
#==ObjectError==
#Level1
totalPriceMin.item=상품의 가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
#Level2 - 생략
totalPriceMin=전체 가격은 {0}원 이상이어야 합니다. 현재 값 = {1}
#==FieldError==
#Level1
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
#Level2 - 생략
#Level3
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} 까지의 숫자를 허용합니다.
#Level4
required = 필수 값 입니다.
min= {0} 이상이어야 합니다.
range= {0} ~ {1} 범위를 허용합니다.
max= {0} 까지 허용합니다.
크게 객체 오류와 필드 오류를 나누고, 범용성에 따라 레벨을 나누었다.
실행하면, 레벨 1 → 2 → 3 → 4 순으로 해당하는 메시지를 찾는다.
이렇게 되면 크게 중요하지 않은 오류 메시지는 기존에 정의된 것을 그냥 재활용한다!
⬇ Level 1
주석
⬇ Level 2, 3
주석
레벨이 낮은 것을 주석해보면 점점 범용성이 큰 메시지가 출력되는 것을 발견할 수 있다!
📌 검증 오류 코드 종류
검증 오류 코드는 다음과 같이 2가지로 나눌 수 있다.
- 개발자가 직접 설정한 오류 코드 →
rejectValue()
를 직접 호출- 스프링이 직접 검증 오류에 추가한 경우 (주로 타입 정보가 맞지 않는 경우)
지금까지는 우리(개발자)가 직접 설정한 오류 코드에 대해 알아보았다.
이제부터는 스프링이 직접 검증 오류에 추가한 경우를 살펴보면서 메시지 코드 전략의 강점을 확인해보자!😉
price
필드에 문자를 입력해보면,
errors.properties
에 타입 오류와 관련된 메시지 코드를 작성하지 않았기 때문에,
스프링이 생성한 기본 메시지가 출력된다.
로그를 살펴보면 다음과 같이 출력된다.
codes[typeMismatch.item.price,typeMismatch.price,typeMismatch.java.lang.Integer,typeMismatch]
이는 스프링이 타입 오류가 발생하면 typeMismatch
라는 오류 코드를 사용하기 때문이고,
이 오류 코드가 MessageCodesResolver
를 통해 4가지 메시지 코드로 생성된 것이다!
하지만 일반 사용자에게 저런 오류 메시지를 출력하는 것은 너무 불친절하다!😣
따로 errors.properties
에 관련 내용을 추가해주자!
errors.properties
에 내용 추가typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다.
실행해보면
우리가 원하는대로 잘 출력되는 것을 확인할 수 있다!
Validator
분리1오류 메시지와 관련된 코드는 많이 깔끔해졌지만,
검증과 관련된 코드는 아직 매우 길다.😨
검증과 관련된 코드를 ItemValidator
클래스로 별도로 분리해서 관리하자!
스프링은 검증을 체계적으로 제공하기 위해서 다음 인터페이스를 제공한다.
public interface Validator {
boolean supports(Class<?> clazz);
void validate(Object target, Errors errors);
}
supports() {}
: 해당 검증기를 지원하는 여부 확인validate(Object target, Errors errors)
: 검증 대상 객체와 BindingResult
Validator
분리2스프링이 Validator
인터페이스를 별도로 제공하는 이유는 체계적으로 검증 기능을 도입하기 위해서다.
앞에서는 검증기를 직접 불러서 사용했고, 이렇게 사용해도 되기는 하지만!
Validator
인터페이스를 사용해서 검증기를 만들면 스프링의 추가적인 도움을 받을 수 있다.
WebDataBinder
를 통해서 사용하기WebDataBinder
는 스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다.
@InitBinder
public void init(WebDataBinder dataBinder) {
log.info("init binder {}", dataBinder);
dataBinder.addValidators(itemValidator);
}
이렇게 WebDataBinder
에 검증기를 추가하면 해당 컨트롤러에서 검증기를 자동으로 적용할 수 있다.
@InitBinder
은 해당 컨트롤러에만 영향을 주기 때문에, 글로벌 설정은 별도로 해야한다.
@Validated
적용@Validated
는 검증기를 실행하라는 애노테이션으로, 이 애노테이션이 붙으면 앞서 WebDataBinder
에 등록한 검증기를 찾아서 실행한다.
만약 여러 검증기를 등록한다면 실행할 검증기를 구분하기 위해 위에서 언급한 supports()
를 사용한다.