메시지와 언어
모든 오류 응답에는 프로그램이 분기할 수 있는 code 가 있고, 문구는 한 응답에 한 언어로 나갑니다:
Accept-Language의 언어를 순서대로 보아 애플리케이션이 번역한 언어(classpath 의messages_<언어>, 도메인 번들 옆<도메인>-messages_<언어>,ValidationMessages_<언어>파일이 있는 언어), 기본 언어, 영어 중 처음 나온 것.- 그런 것이 없으면(헤더가 없거나 번역이 없는 언어) 기본 언어: Spring Boot 의
spring.web.locale(Boot 3·4 같은 이름). 설정하지 않았으면 번들의 기본 문구(영어, 또는 DSL 에 적은 문장). - Bean Validation 의 기본 메시지(
Size,NotNull,Min...)도 같은 언어로, 숫자를 넣어 다시 말합니다 (size must be between 0 and 10/크기가 0에서 10 사이여야 합니다). DSL 에 적은 문장은 적은 그대로입니다.
서버 머신의 언어는 쓰지 않습니다. 한국어 OS 의 서버도 헤더 없는 요청, en, sv 에 영어로 답합니다
(Pet Store 를 -Duser.language=ko -Duser.country=KR 로 띄워 확인).
application.yml - 헤더 없는 요청을 한국어로
spring:
web:
locale: ko
영어는 번들의 기본 문구라서, spring.web.locale: ko 여도 Accept-Language: en 이면 영어입니다. 세션·쿠키·fixed 같은
다른 LocaleResolver 를 쓰면 그 resolver 가 언어를 정합니다.
번역하는 법
- 빌드하면 도메인마다
src/main/resources/i18n/<도메인>-messages.properties가 생깁니다 - 그 도메인이 답할 수 있는 모든 코드와 기본 문구. 생성물이니 고치지 마세요. - 같은 폴더에 내 파일
<도메인>-messages_ko.properties를 두고 필요한 키만 번역합니다(UTF-8 로 저장해도 됨). 빠진 키는 기본 문구로 나갑니다.
src/main/resources/i18n/petstore-messages_ko.properties (Pet Store, 일부)
# Korean wording of petstore-messages.properties (generated beside it).
# The keys come from that file; a key missing here is said in its default wording.
error.not-found.title=찾을 수 없음
error.invalid.title=잘못된 요청
error.state.title=지금 상태에서는 할 수 없는 요청
error.not-signed-in.title=로그인이 필요합니다
error.forbidden.title=권한이 없습니다
error.unique.title=이미 있음
error.unique=이미 사용 중인 값입니다.
invariant.Item.unitCost.min=가격은 0보다 커야 합니다
invariant.PurchaseOrder.lines.minsize=장바구니가 비어 있습니다
state.PurchaseOrder.status.cancel=주문 상태가 {0}이면 취소할 수 없습니다 (가능한 상태: {1})
state.PurchaseOrder.status.ship=주문 상태가 {0}이면 배송할 수 없습니다 ( 가능한 상태: {1})
# A field's check (400), by Spring's message codes: <Constraint>.<field>
Size.lines=장바구니가 비어 있습니다
NotNull.lines=장바구니가 비어 있습니다
# What Spring MVC refuses before a controller runs (RequestShapeErrors reads this file too)
error.unreadable=요청 본문이 없거나 JSON 이 아닙니다.
error.method-not-allowed={0} 메서드는 여기서 쓸 수 없습니다.
typeMismatch.lines.quantity=수량은 0 이상의 정수입니다
POST /api/petstore/purchaseOrder/{id}/cancel
Accept-Language: ko
HTTP/1.1 409
{ "title": "지금 상태에서는 할 수 없는 요청",
"detail": "주문 상태가 SHIPPED이면 취소할 수 없습니다 (가능한 상태: [PLACED])",
"code": "state.PurchaseOrder.status.cancel", "status": 409 }
키
| 키 | 무엇 |
|---|---|
error.<code>.title, error.<code> | 공통 코드의 제목과 문장 |
state.<Entity>.<field>.<command> | 생명주기 거절 - {0} 지금 상태, {1} 가능한 상태 |
invariant.<Entity>.<field>.min ... | 필드 범위 |
reference.<Entity>.<relation> | 가리킨 행이 없음 - {0} 식별자 |
<제약>.<필드> (Size.lines, NotNull.customerDTO.login) | 필드 검증(400), Spring 의 메시지 코드: {0} 필드, 그다음 제약 속성의 이름순 (Size 는 {1} max, {2} min) |
typeMismatch.<필드> | 타입이 받지 않는 값. typeMismatch.lines[0].quantity 는 그 줄만, typeMismatch.lines.quantity 는 모든 줄 |
| 직접 만든 코드 | 손으로 쓴 거절에 준 코드 |
- 값이 들어가는 문장(
state.*,reference.*)은'를''로 적습니다(JavaMessageFormat). - 필드 없는 코드(
Size=,NotNull=)는 쓰지 마세요. 그 언어의 그 제약에 대한 DSL 문장을 모두 덮어 버립니다. ValidationMessages*.properties로 언어를 맞추지 마세요. 기본 파일은 Hibernate Validator 의 번역을 덮고,_<언어>파일만 두면 다른 언어가 서버 머신의 언어로 fallback 합니다. 위의 규칙이 이미 한 응답을 한 언어로 맞춥니다.- 찾는 순서: classpath 의
messages_<언어>.properties, 도메인 번들과 그 옆 번역, 애플리케이션의MessageSource, 기본 문구. 도메인 코드의 번역은messages_<언어>.properties나 도메인 번들 옆에 둡니다 -spring.messages.basename으로 바꾼 번들은 도메인 번들보다 뒤에 찾습니다.
손으로 쓴 거절을 번역하기
throw new ConflictException("loan.no-copy-left", "No copy of {0} is left to lend", isbn);
i18n/library-messages_ko.properties (내 파일) 또는 messages_ko.properties
loan.no-copy-left=빌려줄 수 있는 {0} 이(가) 없습니다
기본 문구는 코드에 적습니다 - 빌드마다 다시 쓰이는 생성 파일 <도메인>-messages.properties 에 적지 않습니다.