악덕? 기획자가 화면에 보이는 문구가 마음에 들지 않는다고, 상품명이라는 단어를 모두 상품이름으로 고쳐달라고 하면 어떻게 해야할까?
여러 화면에 보이는 상품명, 가격, 수량 등, label
에 있는 단어를 변경하려면 다음 화면들을 다 찾아가면서 모두 변경해야 한다. 작은 웹 어플리케이션에서는 크게 중요하지 않은데 규모가 커지면 이런 것들을 일관성 있게 관리하는 것이 매우 중요해진다. 지금처럼 화면 수가 적으면 문제가 되지 않지만 화면이 수십개 이상이라면 수십개의 파일을 모두 고쳐야 되는거다.
addForm.html
, editForm.html
, item.html
, items.html
왜냐하면 해당 HTML 파일에 메시지가 하드코딩 되어 있기 때문이다.
이런 다양한 메시지를 한 곳에서 관리하도록 하는 기능을 메시지 기능이라 한다.
예를 들어서 messages.properties
라는 메시지 관리용 파일을 만들고
item=상품
item.id=상품 ID
item.itemName=상품명
item.price=가격
item.quantity=수량
각 HTML들은 다음과 같이 해당 데이터를 key 값으로 불러서 사용하는 것이다.
<label for="itemName" th:text="#{item.itemName}"></label>
<label for="itemName" th:text="#{item.itemName}"></label>
메시지에서 설명한 메시지 파일( messages.properties
)을 각 나라별로 별도로 관리하면 서비스를 국제화 할 수 있다.
예를 들어서 다음과 같이 2개의 파일을 만들어서 분류한다.
message_en.properties
label.item=Item
label.item.id=Item ID
label.item.itemName=Item Name
label.item.price=price
label.item.quantity=quantity
message_ko.properties
item=상품
item.id=상품 ID
item.itemName=상품명
item.price=가격
item.quantity=수량
영어를 사용하는 사람이면 messages_en.properties
를 사용하고,
한국어를 사용하는 사람이면 messages_ko.properties
를 사용하게 개발하면 된다.
이렇게 하면 사이트를 국제화 할 수 있다.
한국에서 접근한 것인지 영어에서 접근한 것인지는 인식하는 방법은 HTTP accept-language
해더 값을
사용하거나 사용자가 직접 언어를 선택하도록 하고, 쿠키 등을 사용해서 처리하면 된다.
메시지와 국제화 기능을 직접 구현할 수도 있겠지만, 스프링은 기본적인 메시지와 국제화 기능을 모두 제공한다. 그리고 타임리프도 스프링이 제공하는 메시지와 국제화 기능을 편리하게 통합해서 제공한다. 지금부터 스프링이 제공하는 메시지와 국제화 기능을 알아보자.
스프링은 기본적인 메시지 관리 기능을 제공한다.
메시지 관리 기능을 사용하려면 스프링이 제공하는 MessageSource
를 스프링 빈으로 등록하면 되는데, MessageSource
는 인터페이스이다. 따라서 구현체인 ResourceBundleMessageSource
를 스프링 빈으로 등록하면 된다.
직접 등록
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("messages", "errors");
messageSource.setDefaultEncoding("utf-8");
return messageSource;
}
basenames
: 설정 파일의 이름을 지정한다.messages
로 지정하면 messages.properties
파일을 읽어서 사용한다.messages_en.properties
, messages_ko.properties
와 같이 파일명 마지막에 언어 정보를 주면된다. 만약 찾을 수 있는 국제화 파일이 없으면 messages.properties
(언어정보가 없는 파일명)를 기본으로 사용한다./resources/messages.properties
에 두면 된다.messages
, errors
둘을 지정했다. defaultEncoding
: 인코딩 정보를 지정한다. utf-8
을 사용하면 된다.스프링 부트
스프링 부트를 사용하면 스프링 부트가 MessageSource
를 자동으로 스프링 빈으로 등록한다.
스프링 부트 메시지 소스 설정
스프링 부트를 사용하면 다음과 같이 메시지 소스를 설정할 수 있다. application.properties
spring.messages.basename=messages,config.i18n.messages
스프링 부트 메시지 소스 기본 값
spring.messages.basename=messages
MessageSource
를 스프링 빈으로 등록하지 않고, 스프링 부트와 관련된 별도의 설정을 하지 않으면 messages
라는 이름으로 기본 등록된다. 따라서 messages_en.properties
, messages_ko.properties
, messages.properties
파일만 등록하면 자동으로 인식된다.
메시지 파일을 만들어보자. 국제화 테스트를 위해서 messages_en
파일도 추가하자.
messages.properties
:기본 값으로 사용(한글)messages_en.properties
: 영어 국제화 사용/resources/messages.properties
messages.properties
hello=안녕
hello.name=안녕 {0}
/resources/messages_en.properties
messages_en.properties
hello=hello
hello.name=hello {0}
MessageSource 인터페이스
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 org.springframework.context.NoSuchMessageException;
import java.util.Locale;
import static org.assertj.core.api.Assertions.*;
@SpringBootTest
public class MessageSourceTest {
@Autowired
MessageSource ms;
@Test
void helloMessage() {
String result = ms.getMessage("hello", null, null); // locale 정보가 없으므로 디폴트가 리턴된다.
assertThat(result).isEqualTo("안녕");
}
@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("기본 메시지");
}
@Test
void argumentMessage() {
String message = ms.getMessage("hello.name", new Object[]{"Spring"}, null);
assertThat(message).isEqualTo("안녕 Spring");
}
@Test
void defaultLang() {
assertThat(ms.getMessage("hello", null, null)).isEqualTo("안녕");
assertThat(ms.getMessage("hello", null, Locale.KOREA)).isEqualTo("안녕");
}
@Test
void enLang() {
assertThat(ms.getMessage("hello", null, Locale.ENGLISH)).isEqualTo("hello");
}
}
하나씩 살펴보자.
ms.getMessage("hello", null, null)
code: hello
args: null
locale: null
가장 단순한 테스트는 메시지 코드로 hello
를 입력하고 나머지 값은 null
을 입력했다.
locale
정보가 없으면 basename
에서 설정한 기본 이름 메시지 파일을 조회한다. basename
으로 messages
를 지정 했으므로 messages.properties
파일에서 데이터 조회한다.
NoSuchMessageException
이 발생한다.defaultMessage
)를 사용하면 기본 메시지가 반환된다.{0}
부분은 매개변수를 전달해서 치환할 수 있다. hello.name=안녕 {0}
-> Spring 단어를 매개변수로 전달 -> 안녕 Spring
en_US
의 경우 messages_en_US
, messages_en
, messages
순서로 찾는다.Locale
에 맞추어 구체적인 것이 있으면 구체적인 것을 찾고, 없으면 디폴트를 찾는다고 이해하면 된다.ms.getMessage("hello", null, null)
: locale
정보가 없으므로 messages
를 사용ms.getMessage("hello", null, Locale.KOREA)
: locale
정보가 있지만, message_ko
가 없으므로 messages
를 사용ms.getMessage("hello", null, Locale.ENGLISH)
: locale
정보가 Locale.ENGLISH
이므로 messages_en
을 찾아서 사용참고) 근데 언어를 어떻게 구분할까?
HTTP 메시지에 Accept-language를 가지고 웹 브라우저에서 언어를 구분 후 해당 파일을 불러서 html을 렌더링한다. 물론 이런 파일들은 개발자가 직접 개발을 해야한다.
참고
Locale 정보가 없는 경우 Locale.getDefault() 을 호출해서 시스템의 기본 로케일을 사용합니다.
예) locale = null 인 경우 시스템 기본 locale 이 ko_KR 이므로 messages_ko.properties 조회 시도 -> 조회 실패 -> messages.properties 조회
https://www.inflearn.com/questions/286899, https://www.inflearn.com/questions/489062
근데 생각해보면 웹 어플리케이션에 이러한 메시지 소스를 적용할려면 고민할 것들이 좀 많아진다... 그러나 그런 고민을 할 필요가없다. 이러한 것들은 기본 기능이기 때문에 프레임워크들이 그런것들을 다 자동화를 해준다, 따라서, 웹 어플리케이션에서 메시지를 적용하는 방법은 아주 쉽다.
타임리프 메시지 적용
타임리프의 메시지 표현식 #{...}
를 사용하면 스프링의 메시지를 편리하게 조회할 수 있다. 예를 들어서 방금 등록한 상품이라는 이름을 조회하려면 #{label.item}
이라고 하면 된다
참고로 파라미터는 다음과 같이 사용할 수 있다.
hello.name=안녕 {0}
<p th:text="#{hello.name(${item.itemName})}"></p>
ex) th:text="#{label.item.itemName}"
=> 타임리프가 스프링의 메시지 소스를 찾는다.
즉, 스프링 메시지 소스에
(assertThat(ms.getMessage("hello", null, Locale.ENGLISH)).isEqualTo("hello");)
을 호출하는 것이다.
그러면서 Locale 정보까지 찾아서 요청언어에 맞는 정보를 사용해준다.
웹으로 확인하기
웹 브라우저의 언어 설정 값을 변경하면서 국제화 적용을 확인해보자.
크롬 브라우저 설정 언어를 검색하고, 우선 순위를 변경하면 된다.
우선순위를 영어로 변경하고 테스트해보자.
참고
웹 브라우저의 언어 설정 값을 변경하면 요청시Accept-Language
의 값이 변경된다.
Accept-Language 는 클라이언트가 서버에 기대하는 언어 정보를 담아서 요청하는 HTTP 요청 헤더이다.
스프링의 국제화 메시지 선택
앞서 MessageSource 테스트에서 보았듯이 메시지 기능은 Locale 정보를 알아야 언어를 선택할 수 있다. 결국 스프링도 Locale 정보를 알아야 언어를 선택할 수 있는데, 스프링은 언어 선택시 기본으로 Accept-Language 헤더의 값을 사용한다.
Accept-language를 통한 언어 변경말고 사용자가 언어를 선택하게 해서 쿠키나 세션같은 곳에 언어를 선택해놓고 계속 쓸 수 있게도 할 수 있다.
참고 : 쿠키나 세션을 쓰는 이유
사용자가 직접 Accept-language를 바꾸는 일은 거의 없으니깐..
스프링은 Locale 선택 방식을 변경할 수 있도록 LocaleResolver
라는 인터페이스를 제공하는데, 스프링 부트는 기본으로 Accept-Language 를 활용하는 AcceptHeaderLocaleResolver
를 사용한다.
즉, 웹브라우저에서 설정과 상관없이 팝업 같은 것을 이용하기 위해선 LocaleResolver
를 사용한다고 이해하자.
LocaleResolver 인터페이스
public interface LocaleResolver {
Locale resolveLocale(HttpServletRequest request);
void setLocale(HttpServletRequest request, @Nullable HttpServletResponse
response, @Nullable Locale locale);
}
LocaleResolver 변경
만약 Locale 선택 방식을 변경하려면 LocaleResolver
의 구현체를 변경해서 쿠키나 세션 기반의 Locale 선택 기능을 사용할 수 있다. 예를 들어서 고객이 직접 Locale 을 선택하도록 하는 것이다. 관련해서 LocaleResolver
를 검색하면 수 많은 예제가 나오니 필요한 분들은 참고하자.
참고
메시지 기능과 달리 국제화 기능같은 경우는 글로벌 사이트 관련 개발을 해야 사용한다. 그러니깐 필요한 상황에선 관련된 예제가 많으니 그것들을 그때그때 찾아서 해보자.