어느날, 신나게 개발을 진행하다가 화면에 보이는 문구가 마음에 들지않고 자꾸만 신경쓰일 때가 있다. 그런데 그 문구는 전부 코드안에 때려박혀있는 데다가(하드코딩) 동일한 문구가 한 두군데가 아니어서 쉽사리 건들기도 어렵거니와 까딱하면 일부만 수정되는 경우가 생길 수도 있다.
그래서 이 현상을 방지하고자, Spring에서는 그런 문구를 따로 관리할 수 있도록 기능을 제공해주는데, 이를 메시지 기능이라고 한다. 또한, 이것을 확장하여 현재 접속하고 있는 국가에 따라 (Locale 정보) 다른 화면을 보여줄 수 있는 기능 또한 제공해주는데, 이를 국제화 기능이라고 한다.
@Configuration
public class MessageConfig {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("messages", "errors");
messageSource.setDefaultEncoding("utf-8");
return messageSource;
}
}
# 메시지 파일이 있는 경로 + default 이름 설정
# - Default: messages
# - , 기호를 통해 여러 경로를 설정할 수 있다.
# - . 기호를 통해 path를 설정할 수 있다.
# - config.messages.messages
# - src/main/resources/config/messages/messages*.properties
# - Prefix: classpath:/resources/
# - Suffix: .properties
spring.messages.basename = messages
# 메시지 파일의 인코딩을 설정
# - Default: utf-8
spring.messages.encoding = utf-8
# 메시지의 인수({0}, {1})를 지원 안 할지 여부
# - true: 인수 지원 안함, false: 인수 지원 함.
# - Default: false
spring.messages.always-use-message-format = false
# 메시지 파일의 캐싱 기간
# 설정하지 않으면 영구히 캐싱된다.
# 기간 접미사(h,m,s)를 설정하지 않으면 기본적으로 초(s)를 사용한다.
# - Default: forever
spring.messages.cache-duration = -1
# 특정 Locale에 대한 파일을 찾지 못한 경우, System Locale을 사용할지 설정
# false로 설정하면 파일을 찾지 못하는 경우, 무조건 messages.properties를 찾는다.
# - Default: true
spring.messages.fallback-to-system-locale = true
# "NoSuchMessageException" 대신 메시지 코드를 기본 메시지로 사용할지 여부
# 공식 문서에 따르면 운영 환경에서는 false로 설정하라고 이야기 한다.
# - Default: false
spring.messages.use-code-as-default-message = false
Spring Boot는 MessageSource를 자동으로 Spring Bean으로 등록한다. 그래서 application.properties에 설정만 하면 된다.
spring.messages.basename
,
기호를 통해 여러 경로를 설정할 수 있다..
기호를 통해 경로를 설정할 수 있다.classpath:/resources/
.properties
messages
spring.messages.encoding
UTF-8
spring.messages.always-use-message-format
{0}
, {1}
)를 사용하지 않을지를 설정false
spring.messages.cache-duration
forever
spring.messages.fallback-to-system-locale
messages.properties
를 찾는다.true
spring.messages.use-code-as-default-message
false
이름 | 설명 | 기본값 |
---|---|---|
basename | 메시지 파일의 경로 + 기본 이름 설정 | messages |
encoding | 메시지 파일의 기본 인코딩 | UTF-8 |
always-use-message-format | {0}, {1}를 지원 안 할지 여부 | false |
cache-duration | 캐싱 기간 | forever |
fallback-to-system-locale | 특정 Locale에 대한 파일을 찾지 못한 경우, 시스템 로케일로 돌아갈지 여부 | true |
use-code-as-default-message | 메시지 파일을 찾지 못했을 때, 예외 처리 대신 메시지 코드를 그대로 반환할지 여부 | false |
package org.springframework.context;
public interface MessageSource {
@Nullable
String getMessage(
String code,
@Nullable Object[] args,
@Nullable String defaultMessage,
Locale locale
);
String getMessage(
String code,
@Nullable Object[] args,
Locale locale
) throws NoSuchMessageException;
String getMessage(
MessageSourceResolvable resolvable,
Locale locale
) throws NoSuchMessageException;
}
코드를 작성할땐 MessageSource Interface를 통해 코드를 작성하며, Spring Boot는 컴파일될때 MessageSource의 구현체인ResourceBundleMessageSource
를 주입하여 사용된다.
{0}
, {1}
)에 들어갈 메시지들을 넣는다.new Object[]{"...", "..."}
@NotNull
), null을 전달한 경우 Locale.getDefault()를 호출해 Spring이 실행되고 있는 환경의 Locale를 바탕으로 검색한다.spring.messages.fallback-to-system-locale
을 false로 설정한 경우 messages.properties를 바로 찾는다. 이마저도 없으면 NoSuchMessageException이 발생한다.Locale값에 따라 messages_* 파일을 검색하는데, 구체적인 것부터 기본까지 차례대로 검색한다.
예를 들어서 Locale값으로 EN_US가 왔다고 가정해보자.
순으로 파일을 검색하게 된다.
# messages.properties
hello = 안녕
hello.name = 안녕 {0}
# messages_en.properties
hello = hello
hello.name = hello {0}
properties 파일의 특성상, key = value
형식으로 작성하면 된다.
MessageSource.getMessage
의 code
파라미터는 key
값을 토대로 찾는다.
value
에 {0}
, {1}
파라미터를 사용하면, Object[] args
파라미터에 입력된 순서대로 매핑을 시킬 수 있다.
@SpringBootTest
public class MessageSourceTest {
@Autowired
MessageSource ms;
}
Spring에서 사용하기 위해선 MessageSource를 주입해주면 된다. 위의 코드는 테스트 코드이기 때문에 필드에 바로 주입을 해주었는데, 일반 Component의 경우에는 생성자를 통해 주입하자.
@SpringBootTest
public class MessageSourceTest {
@Autowired
MessageSource ms;
/**
* code = hello
* args = null
* locale = null
* => 기본값인 messages.properties
*/
@Test
@DisplayName("메시지 가져오기")
void helloMessage() {
assertThat(
ms.getMessage("hello", null, null)
).isEqualTo("안녕");
}
/**
* "code"가 없는 경우,
* "NoSuchMessageException" 발생
*/
@Test
@DisplayName("메시지가 없는 경우")
void notFoundMessageCode() {
assertThatThrownBy(
() -> ms.getMessage("no_code", null, null)
).isInstanceOf(NoSuchMessageException.class);
}
/**
* 3번째 인자에 "defaultMessage"를 설정해주면,
* "code"가 없을 경우 "defaultMessage"를 반환
*/
@Test
@DisplayName("Default 메시지를 설정한 경우")
void notFoundMessageCodeDefaultMessage() {
assertThat(
ms.getMessage("no_code", null, "기본 메시지", null)
).isEqualTo("기본 메시지");
}
/**
* 2번째 인자에 "new Object[]{}"을 이용해 인자를 줄 수 있다.
* - hello.name = 안녕 {0}
* - => 안녕 Spring
*/
@Test
@DisplayName("매개 변수 사용")
void argumentMessage() {
assertThat(
ms.getMessage("hello.name", new Object[]{"Spring"}, null)
).isEqualTo("안녕 Spring");
}
/**
* "Locale"를 기반으로 국제화 파일을 선택한다.
* - Locale=en_US => messages_en_US -> messages_en -> messages 순으로 찾는다.
* 1. "Locale.CHINA"는 없으니 기본값 선택
* 2. "Locale.ENGLISH"는 있으니 기본값 선택 X
*/
@Test
@DisplayName("국제화 파일 선택")
void langMessage() {
assertThat(
ms.getMessage("hello", null, Locale.CHINA)
).isEqualTo("안녕");
assertThat(
ms.getMessage("hello", null, Locale.ENGLISH)
).isNotEqualTo("안녕");
assertThat(
ms.getMessage("hello", null, Locale.ENGLISH)
).isEqualTo("hello");
}
}
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
</head>
<body>
<div>
<table>
<tr>
<th>[[#{label.item.id}]]</th>
<th th:text="#{label.item.itemName}"></th>
</tr>
</table>
</div>
</body>
</html
Thymeleaf에서 #{...}
를 이용해 메시지 파일을 직접 접근할 수 있다. 이 때는 Locale 정보도 자동으로 넘어간다.