코드에 직접 적힌 코드를 일일이 수정하는 것은 보통일이 아니다.
만약 기획자가 html의 라벨 속 상품명 이란 단어를 모두 상품이름으로 변경해 달라고 하면 어떻게 해야할까? 일일이 고치면 될 것 같아 보이지만, 많은 코드들을 실수없이 변경하는 것은 쉬운일이 아니다.
이처럼 html에 하드코딩 되어 있는 단어들을 변경하는 것은 쉽지 않다. 대신, 이같은 다양한 문자들을 한 곳에서 관리하는 기능을 메시지 기능이라 한다.
예를들어 message.properties
라는 메시지 관리용 파일을 만들고
item = 상품
item.id = 상품 ID
item.itemName = 상품명
item.price = 가격
item.quantity = 수량
(key = value)
이제 HTML에서는 각 데이터들을 key 값으로 불러서 사용하면된다.
<label for="itemName" th:text="#{item.itemName}"></label>
<label for="itemName" th:text="#{item.itemName{"></label>
이처럼 서로 다른 파일이더라도 하드코딩을 하지 않고 메시지 기능을 사용하면, 메시지 관리용 파일의 값을 바꾸는 것 만으로 한번에 모든 글자들을 변경할 수 있다.
작은 애플리케이션에서는 크게 중요하지 않으나, 프로젝트의 크기가 커지면 이같은 일관성있는 관리가 매우 중요해진다.
한국에서 들어가면 한국어로 보이고, 미국에서 들어가면 영어로 보이고, 독일에서 접속하면 독일어로 보이는 기능. 이 같은 국제화 기능은 메시지에서 조금만 더 나가면 된다. 이는 메시지 파일을 나라별로 관리하고, 영어이면 영어로 작성된 파일을, 한국이면 한국어로 작성된 파일을 제공하는 것이다. 이런 식으로 사이트를 국제화할 수 있다.
item = 상품
item.id = 상품 ID
item.itemName = 상품명
item.price = 가격
item.quantity = 수량
item = item
item.id = item.id
item.itemName = item.itemName
item.price = item.price
item.quantity = item.quantity
언어 인식 방법 1. HTTP의 accept-language
헤더 값 사용
언어 인식 방법 2. 사용자가 직접 언어 선택
스프링은 기본 국제화 기능을 제공하고 있다. 타임리프도 메시지, 국제화 기능을 편리하게 통합해서 제공한다.
메시지 관리 기능을 사용하려면 스프링이 제공하는 MessageSource
(인터페이스)를 스프링 빈으로 등록하면 된다. 따라서, 구현체인 ResurceBundleMessageSource
를 스프링 빈으로 등록하면 된다.
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("messages", "errors");
messageSource.setDefaultEncoding("utf-8");
return messageSource;
}
basenames
: 설정 파일의 이름을 지정한다.message
로 지정하면 message.properties
파일을 읽어서 사용한다.message_ko.properties
, message_en.properties
와 같이 파일명 마지막에 언어 정보를 주면 된다. 만약 찾을 수 있는 국제화 파일이 없으면 message.properties
(언어 정보가 없는 파일명)를 기본으로 사용한다./resources/message.properties
에 두면 된다.messages
, errors
둘을 지정했다.defaultEncoding
: 인코딩 정보를 지정한다. utf-8
을 사용하면 된다.스프링 부트를 사용하면 스프링 부트가 MessageSource
를 자동으로 스프링 빈으로 등록한다.
spring.messages.basename=messages,config.i18n.messages
spring.messages.basename=messages
MessageSource
를 스프링 빈으로 등록하지 않고, 스프링 부트와 관련된 별도의 설정을 하지 않으면 messages
라는 이름으로 기본 등록한다. 따라서 message_en.properties
, message_ko.properties
, message.properties
파일만 등록하면 자동으로 인식된다.
메시지 파일을 만들어보자!
message.properties
: 기본값으로 사용(한글)message_en.properties
: 영어 국제화 사용주의! 파일명은 mesage가 아니라 messages다! 마지막에 s에 주의하자
hello=안녕
hello.name=안녕 {0}
hello=hello
hello.name=hello {0}
이렇게 파일명을 설정하면 intellij가 보기좋게 Resrce Bundle 'messages'로 묶어서 보여준다.
public interface MessageSource {
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
MessageSource
가 스프링 빈에 등록되고, 우리는 이것을 가져다 테스트에서 사용해볼 것이다.
package hello.itemservice.message;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.MessageSource;
import static org.assertj.core.api.Assertions.*;
@SpringBootTest
public class MessageSourceTest {
@Autowired
MessageSource ms;
@Test
void helloMessage() {
String result = ms.getMessage("hello", null, null);
assertThat(result).isEqualTo("안녕");
}
}
ms.getMessage("hello", null, null)
hello
null
(변수 순서)null
(국제화에 사용되는 변수)메시지 코드로 hello
를 입력하고 나머지 값은 null
을 입력했다. locale
정보가 없으면 basename
에서 설정한 기본 이름(message
) 메시지 파일을 조회한다. 따라서 message.properties
파일에서 데이터를 조회한다.
@Test
void notFoundMessageCode() {
assertThatThrownBy(() -> ms.getMessage("no_code", null, null))
.isInstanceOf(NoSuchMessageException.class);
}
@Test
void notFoundMessageCodeDefaultMessage() {
String result = ms.getMessage("no_code", null, "기본 메시지", null);
assertThat(result).isEqualTo("기본 메시지");
}
ms.getMessage("no_code", null, "기본 메시지", null);
NoSuchMessageExeption
이 발생한다.defaultMessage
)를 사용하면 기본 메시지가 반환된다.@Test
void argumentMessage() {
String result = ms.getMessage("hello.name", new Object[]{"Spring"}, null);
assertThat(result).isEqualTo("안녕 Spring");
}
ms.getMessage("hello.name", new Object[]{"Spring"}, null);
new Object[]{"spring"}
으로 배열을 넘긴다.locale 정보를 기반으로 국제화 파일을 선택한다.
en_US
의 경우 message_en_US
-> message_en
-> message
순서로 찾는다.Locale
에 맞추어 구체적인 것이 있으면 구체적인것을 찾고, 없으면 디폴트를 찾는다고 이해하면 된다.@Test
void defaultLang() {
assertThat(ms.getMessage("hello", null, null)).isEqualTo("안녕");
// locale 정보가 없으므로 messages 를 사용
assertThat(ms.getMessage("hello", null, Locale.KOREA)).isEqualTo("안녕");
// locale 정보가 있지만, message_ko 가 없으므로 messages 를 사용
@Test
void enLang() {
assertThat(ms.getMessage("hello", null, Locale.ENGLISH)).isEqualTo("hello");
// locale 정보가 Locale.ENGLISH 이므로 messages_en 을 찾아서 사용
}
messages.properteis
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=취소
타임리프의 메시지 표현식 #{...}
를 사용하면 스프링의 메시지를 편리하게 조회할 수 있다. 예를 들어서 방금 등록한 상품이라는 이름을 조회하려면 #{label.item}
이라고 하면 된다.
<div th:text="#{label.item}"></h2>
<div>상품</h2>
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2 th:text="#{page.addItem}">상품 등록</h2>
</div>
<h4 class="mb-3">상품 입력</h4>
<form action="item.html" th:action th:object="${item}" method="post">
<div>
<label for="itemName" th:text="#{label.item.itemName}">상품명</
label>
<input type="text" id="itemName" th:field="*{itemName}"
class="form-control" placeholder="이름을 입력하세요">
</div>
<div>
<label for="price" th:text="#{label.item.price}">가격</label>
<input type="text" id="price" th:field="*{price}" class="form
control" placeholder="가격을 입력하세요">
</div>
<div>
<label for="quantity" th:text="#{label.item.quantity}">수량</label>
<input type="text" id="quantity" th:field="*{quantity}"
class="form-control" placeholder="수량을 입력하세요">
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit"
th:text="#{button.save}">저장</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/message/items}'|"
type="button" th:text="#{button.cancel}">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
<h2>상품 등록 폼</h2>
<h2 th:text="#{page.addItem}">상품 등록</h2>
<label for="itemName">상품명</label>
<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
<label for="price" th:text="#{label.item.price}">가격</label>
<label for="quantity" th:text="#{label.item.quantity}">수량</label>
<button type="submit">상품 등록</button>
<button type="submit" th:text="#{button.save}">저장</button>
<button type="button" th:text="#{button.cancel}">취소</button>
hello.name=안녕 {0}
<p th:text="#{hello.name(${item.itemName})}"></p>
영어 메시지를 추가하자!
messages_en.properties
label.item=Item
label.item.id=Item ID
label.item.itemName=Item Name
label.item.price=price
label.item.quantity=quantity
page.items=Item List
page.item=Item Detail
page.addItem=Item Add
page.updateItem=Item Update
button.save=Save
button.cancel=Cancel
이미 앞에서 템플릿 파엘이 모두 #{...}
를 통해서 메시지 사용을 적용해 두었으므로 국제화 작업이 거의 끝났다!
웹 브라우저의 언어 설정 값을 변경하여 국제화 적용을 확인할 수 있다.
웹 브라우저의 언어 설정 값을 변경하면 요청 시 Accept-Language
의 값이 변경된다.
Accept-Language 클라이언트가 서버에 기대하는 언어 정보를 담아서 요청하는 HTTP 요청 헤더.
앞서 MessageSource
테스트에서 보았듯이 메시지 기능은 Locale
정보를 알아야 언어를 선택할 수 있다. 결국 스프링도 Locale
정보를 알아야 언어를 선택할 수 있는데, 스프링은 언어 선택시 기본으로 Accept-Language
헤더의 값을 사용한다.
그러나 이같은 선택 방식도 바꿀 수 있다. 사용자가 웹 페이지에 들어왔을 때, (웹 브라우저의) 설정과 무관하게 언어를 바꿀 수 있도록 하려면 다른 방식이 필요하다. 이때, LocaleResolver를 사용할 수 있다.
스프링은 Locale
선택 방식을 변경할 수 있도록 LocaleResolver
라는 인터페이스를 제공하는데, 스프링 부트는 기본으로 Accept-Language
를 활용하는 AcceptHeaderLocaleResolver
를 사용한다.
public interface LocaleResolver {
Locale resolveLocale(HttpServletRequest request);
void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);
}
만약 Locale
선택 방식을 변경하려면 LocaleResolver
의 구현체를 변경해서 쿠키나 세션 기반의 Locale
선택 기능을 사용할 수 있다. 웹 브라우저에서 사용자가 언어를 변경하면, 이를 쿠키나 세션에 저장하여 계속 해당 언어로 사이트를 제공할 수 있다. 관련된 예제는 검색해서 사용해보자.