악덕 기획자가 화면에 표시된 문구가 마음에 들지 않아, "상품명"이라는 단어를 모두 "상품이름"으로 수정해달라고 요청했다고 가정해 보자. 이 경우, 여러 화면에 걸쳐 표시된 "상품명", "가격", "수량" 등의 레이블을 일일이 찾아가면서 변경해야 한다. 현재는 페이지가 적어 큰 문제가 되지 않지만, 만약 페이지가 수십 개 이상이라면 수많은 파일을 모두 수정해야 하는 상황이 발생할 수 있다.
원인은 HTML 파일에 메세지가 하드코딩 되어있기 때문이다.
<!-- 주석 표시한 부분이 메세지가 들어갈 자리-->
<form action="item.html" th:action th:object="${item}" method="post">
<div>
<label for="itemName">**<!--상품명--></label>
<input type="text" id="itemName" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
</div>
<div>
<label for="price"><!--가격--></label>
<input type="text" id="price" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요">
</div>
<div>
<label for="quantity"><!--수량--></label>
<input type="text" id="quantity" th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
</div>
<hr class="my-4">
이런 다양한 메시지를 한 곳에서 관리하도록 하는 기능을 메시지 기능이라 한다. messages.properties라는 메시지 관리용 파일을 만들고
item=상품
item.id=상품 ID
item.itemName=상품명
item.price=가격
item.quantity=수량
<label for="itemName" th:text="#{item.itemName}"></label>
<label for="itemName" th:text="#{item.itemName}"></label>
각 HTML들은 다음과 같이 해당 데이터를 key 값으로 불러서 사용한다.
메시지에서 설명한 메시지 파일(messages.properties)을 각 나라별로 별도로 관리하면 국제화를 통해 애플리케이션에서 다국어 메시지를 지원하고, 사용자가 설정한 언어 및 문화권에 맞는 텍스트를 표시할 수 있다.
한국
item=상품
item.id=상품 ID
item.itemName=상품명
item.price=가격
item.quantity=수량
미국
item=Item
item.id=Item ID
item.itemName=Item Name
item.price=price
item.quantity=quantity
HTTP accept-language 헤더 값을 사용하거나 사용자가 직접 언어를 선택하도록 하고, 쿠키 등을 사용해서 처리한다.
스프링 부트를 사용하면 스프링 부트가 MessageSource를 자동으로 스프링 빈으로 등록한다. src/main/resources 경로에 messages.properties, messages_en.properties, messages_ko.properties 등의 파일이 있으면, Spring Boot는 자동으로 해당 파일들을 찾아서 국제화 메시지를 처리한다
messages.properties(default)
hello=안녕
hello.name=안녕 {0}
messages_en.properties
hello=hello
hello.name=hello {0}
메세지 소스를 포함하고 있는 MessageSource가 스프링 빈에 등록되어 사용 가능하다.
Test 코드
@SpringBootTest
public class MessageSourceTest {
@Autowired
MessageSource ms;
@Test
void helloMessage(){
String hello = ms.getMessage("hello", null, null);
Assertions.assertThat(hello).isEqualTo("안녕");
}
}
locale(3번째 argument)이 null이면 default인 messages.properties파일의 key 값을 불러온다.
만약 메세지를 읽지 못하는 에러가 발생한다면 아래와 같이 설정을 UTF-8로 변경한다.
@Test
void notFoundMessageCode() {
Assertions.assertThatThrownBy(() -> ms.getMessage("no_code", null, null))
.isInstanceOf(NoSuchMessageException.class);
}
ms.getMessage("no_code", null, null)를 호출할 때 no_code라는 메시지 키가 존재하지 않아 NoSuchMessageException이 발생한다.
@Test
void notFoundMessageCodeDefaultMessage() {
String result = ms.getMessage("no_code", null, "기본 메시지", null);
Assertions.assertThat(result).isEqualTo("기본 메시지");
}
ms.getMessage("no_code", null, "기본 메시지", null)를 호출할 때 no_code라는 메시지 키가 존재하지 않으면 "기본 메시지"가 반환된다.
@Test
void argumentMessage() {
String result = ms.getMessage("hello.name", new Object[]{"Spring"}, null);
Assertions.assertThat(result).isEqualTo("안녕 Spring");
}
messages.properties에 hello.name=안녕 {0} 으로 설정되어 있기 때문에 argument "Spring"을 hello.name에 넘긴다면 "안녕 Spring"을 반환한다.
@Test
void defaultLang() {
assertThat(ms.getMessage("hello", null, null)).isEqualTo("안녕");
assertThat(ms.getMessage("hello", null, Locale.KOREA)).isEqualTo("안녕");
}
@Test
void enLang() {
Assertions.assertThat(ms.getMessage("hello", null, Locale.ENGLISH)).isEqualTo("hello");
}
나라별 함수를 작성하여 테스트 해보면 정상적으로 통과한다.
label.item=상품
label.item.id=상품 ID
label.item.itemName=상품명
label.item.price=가격
label.item.quantity=수량
page.items=상품 목록
page.item=상품 상세
page.addItem=상품 등록
page.updateItem=상품 수정
button.save=저장
button.cancel=취소
messages.properties에 해당 내용을 추가하고 타임리프의 메시지 표현식 #{...}를 사용하면 스프링의 메시지를 편리하게 조회할 수 있다.
렌더링 전
<div th:text="#{label.item}"></h2>
렌더링 후
<div>상품</h2>
messages_en.properties에 내용을 추가하고 Chrome 설정-> 언어 검색 -> 영어(가장 위로 이동) -> 새로고침 으로 언어를 영어로 변경하여 메세지가 잘 적용됬는지 확인한다.
스프링은 언어 선택시 기본으로 Accept-Language 헤더의 값을 사용하여 브라우저의 설정에 따라 LocaleResolver가 적절한 Locale을 설정한다.