스프링에서 검증 오류가 발생했을 때, BindingResult에 담기는 수많은 에러 코드들은 도대체 어떻게 실제 화면에 출력되는 메시지로 변환되는 것일까?
단순히 "에러가 나면 메시지를 출력한다"는 과정 뒤에 숨겨진 전략(Strategy)과 조회(Lookup)의 분리 메커니즘을 정리해 보았다.
가장 먼저 이해해야 할 핵심은 "키(Key)를 생성하는 역할"과 "값(Value)을 찾는 역할"이 철저히 분리되어 있다는 점이다.
| 구분 | 역할 | 클래스/인터페이스 | 특징 |
|---|---|---|---|
| 전략가(Generator) | 메시지 코드 생성 | MessageCodesResolver | 검증 오류 발생 시 작동. 메시지 파일 유무와 상관없이 규칙에 따라 후보 키(Candidate Keys) 리스트를 생성함. |
| 조회자(Lookup) | 메시지 내용 찾기 | MessageSource | 생성된 리스트를 들고 실제 메시지 설정 파일에서 값을 매칭함. |
오류가 발생하면 리졸버는 가장 구체적인 코드부터 범용적인 코드까지 순서대로 생성한다. 예를 들어 user 객체의 age 필드(int 타입)에 잘못된 값이 들어왔다면, 리졸버는 내부 규칙에 따라 다음과 같은 배열을 만든다.
코드.객체명.필드 (예: typeMismatch.user.age)코드.필드명 (예: typeMismatch.age)코드.필드타입 (예: typeMismatch.java.lang.Integer)코드 (예: typeMismatch)이때 리졸버는 messages.properties을 확인하지 않는다. 오직 약속된 규칙대로 "이 이름들로 한 번 찾아봐"라며 조회용 키 배열만 만들어 에러 객체에 담아줄 뿐이다.
이제 MessageSource가 바통을 이어받는다. 리졸버가 만든 메시지 코드 배열을 들고 설정된 메시지 파일(Resource Bundle)을 위에서부터 훑는다.
스프링 부트의 기본 설정값(basename)은 messages이므로 보통 src/main/resources/messages.properties를 사용한다. 하지만 이는 설정이므로 변경 가능하다.
# application.properties 예시
spring.messages.basename=errors, common # errors.properties와 common.properties를 사용
이처럼 basename을 다르게 설정했다면, MessageSource는 해당 설정 파일들을 대상으로 매칭을 시도한다.
코드.객체명.필드)가 파일에 있나? -> 없으면 패스코드.필드명)가 파일에 있나? -> 없으면 패스코드.필드타입)가 파일에 있나? -> "발견! 해당 메시지 반환"만약 우리가 설정 파일에 아래와 같이 적어두었다면, 3순위에서 매칭이 성공하여 해당 문구가 출력된다.
# errors.properties
typeMismatch.java.lang.Integer=숫자 형식으로 입력해야 합니다.
이 계층 구조에서 오류코드.필드타입 단계가 존재하는 이유는 "공통 처리의 유연성" 때문이다.
required.user.age처럼 특정 필드에만 특화된 메시지를 제공할 수 있다.typeMismatch.java.lang.Integer 하나만 등록해두면 리졸버가 생성한 후보군 덕분에 모든 int 필드 오류가 이 메시지를 공유하게 된다.결국 이 구조는 "구체적인 설정이 있으면 그걸 쓰고, 없으면 범용적인 설정을 Fallback으로 써라"라는 스프링의 유연한 설계가 반영된 결과이다.
만약 개발자가 메시지 파일에 1~4순위 중 아무것도 적지 않았다면 어떻게 될까? MessageSource는 모든 검색에 실패하고, 최종적으로 스프링이 내부적으로 들고 있던 기본 메시지(영문)를 출력하게 된다.
Failed to convert property value of type 'java.lang.String' to required type 'Java.lang.Integer'...
우리가 메시지 파일에 키 값을 등록하는 이유는 바로 이 불친절한 기본 메시지를 친절한 한글 메시지로 재정의(Override)하기 위함이다.
MessageCodesResolver: 검증 오류 발생 시 작동하며, 규칙에 따라 후보 키 리스트를 만든다.MessageSource: basename으로 설정한 메시지 파일을 뒤져서 후보 키 중 일치하는 첫 번째 메시지를 확정한다.typeMismatch.java.lang.Integer): 필드별 메시지가 없을 때, 특정 타입에 대해 공통 안내 문구를 처리하기 위한 효율적인 대안이다.한 줄 평: "리졸버는 조회용 키 리스트를 설계하고, 소스는 설정된 파일에서 정답을 찾는다!"