Spring으로 다국어를 처리하는 방법에 대해 검색해보면, Spring Boot를 사용하는 경우에는 MessageSource가 자동으로 등록되기 때문에 등록하지 않아도 되고, Spring Boot를 사용하지 않는 경우에는 아래와 같이 설정파일에 MessageSource를 등록하라는 글이 많이 나온다.
@Configuration
public class MessageConfig {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("customMessage/messages");
messageSource.setFallbackToSystemLocale(false);
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
}
근데 Spring Boot를 사용하면 어떻게 자동 등록이 되는건지, 기본 세팅된 설정은 어떻게 되는지, 그 설정을 바꿀수 있는건지 이런 부분에 대해서 명확하게 설명한 글은 찾기 어려워서 힘들었다. 그래서 지금까지 알아낸 부분에 대해 정리해보고자 한다.
속성 | 타입 | 설명 | 기본값 |
---|---|---|---|
basename | String | - 쉼표로 구분된 basename 리스트 | "messages" |
encoding | Charset | - 메세지 인코딩 방식 | StandardCharsets.UTF_8 |
cacheDuration | Duration | - 로드된 리소스 번들 파일 캐시 기간. - 단위를 지정하지 않으면 초가 사용됨. | 영구적으로 캐시됨 |
fallbackToSystemLocale | boolean | - 요청받은 Locale에 대한 파일을 찾지 못할 경우 시스템 설정 Locale을 사용할 것인지에 대한 유무 | true |
alwaysUseMessageFormat | boolean | - 전달받은 인자를 제외하고 메세지를 읽어들이는 MessageFormat 규칙을 항상 적용할 것인지에 대한 유무 | false |
useCodeAsDefaultMessage | boolean | - 메세지 파일에 코드에 대해당하는 값이 없을 때 NoSuchMessageException 대신 코드를 메세지로 사용할 것인지에 대한 유무 | false |
설정
위의 표에서 나타낸 것처럼 기본적으로 세팅이 되어 있기 때문에, 변경할 필요가 없다면 설정과 관련해서 아무것도 안해도 된다. 메세지 파일만 만들어주면 된다.
메세지 파일 만들기
basename이 "messages"이기 때문에, resources 폴더 바로 아래 messages.properties 파일을 만들어주면 된다. 다른 locale에 대한 메세지 파일을 만들고 싶다면, 같은 위치에 messages_en.properties 이런식으로 만들어주면 된다. 메세지 파일 안에는 success=성공!
과 같이 키=값 형태로 메세지를 정리해두면된다.
spring.messages.basename=message/messages
spring.messages.encoding=UTF-8
spring.messages.fallbackToSystemLocale=false
spring.messages.alwaysUseMessageFormat=true
spring:
messages:
basename:message/messages
encoding:UTF-8
fallbackToSystemLocale:false
alwaysUseMessageFormat:true
application.properties로 자동설정은 했는데, 어떻게 이게 가능한걸까?
메인메서드가 있는 application 클래스부터 살펴보자. 여기서 주목할 건 @SpringBootApplication
어노테이션이다.
@SpringBootApplication
public class ExampleApplication {
public static void main(String[] args) {
SpringApplication.run(ExampleApplication.class, args);
}
}
해당 어노테이션 클래스로 넘어가보자.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {...}
자동 설정을 위한 @EnableAutoConfiguration
어노테이션과, component를 찾아 bean으로 등록하기 위한 @ComponentScan
어노테이션이 있다.
→ 따라서 굳이 MessageSource를 만들어서 bean으로 등록하지 않아도 자동 설정으로 등록된 bean을 사용할 수 있게 되는 것이다.
그럼 자동 설정되는 MessageSource는 기본적으로 어떤 속성들을 가지고 있을까?
인텔리제이에서 shift
버튼을 두 번 눌러서 AutoConfiguration
을 검색해보면 SpringBoot에서 자동설정을 위해 제공하는 파일들을 확인할 수가 있다.
여기서 messageSourceAutoConfiguration.java
파일로 들어가보면 아래와 같이 2개의 Bean과 조건 확인을 위한 ResourceBundleCondition
클래스로 구성되어있다.
@AutoConfiguration
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {
private static final Resource[] NO_RESOURCES = {};
@Bean
@ConfigurationProperties(prefix = "spring.messages")
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
...
return messageSource;
}
protected static class ResourceBundleCondition extends SpringBootCondition {
private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {...}
private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {...}
private Resource[] getResources(ClassLoader classLoader, String name) {...}
}
}
자 이제 하나씩 뜯어보자!!
MessageSourceAutoConfiguration
에는 @Conditional(ResourceBundleCondition.class)
어노테이션이 붙어있는데, 인자로 넘어온 클래스가 가지고 있는 matches
메서드가 true
를 반환해야 자동설정을 진행한다.
→ ResourceBundleCondition
클래스의 getMatchOutcome
메서드를 통해 조건 체크를 한다.
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages"); // 1. basename 세팅
ConditionOutcome outcome = cache.get(basename); // 2. 캐시에 basename으로 저장된 값이 있는지 확인
if (outcome == null) {
outcome = getMatchOutcomeForBasename(context, basename); // 3. 저장된 값이 없다면 getMatchOutcomeForBasename 호출
cache.put(basename, outcome); // 4. 결과값 cache 변수에 저장
}
return outcome;
}
messages
로 할당한다.getMatchOutcomeForBasename
을 호출하고 결과값을 outcome에 할당한다.private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
ConditionMessage.Builder message = ConditionMessage.forCondition("ResourceBundle");
for (String name : StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename))) { // 1. basename 리스트 돌면서 getResources를 호출
for (Resource resource : getResources(context.getClassLoader(), name)) {
if (resource.exists()) { // 2. getResources의 결과값인 Resource를 확인하면서 존재 유무 확인
return ConditionOutcome.match(message.found("bundle").items(resource)); // 3. 존재한다면, 아래 사진처럼 match가 true인 outcome을 반환
}
}
}
return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll()); // 4. 존재하는 자원이 없다면, match가 false인 outcome을 반환한다.
}
private Resource[] getResources(ClassLoader classLoader, String name) {
String target = name.replace('.', '/');
try { // 클래스패스 내에 basename에 해당하는 properties 파일이 있는지 찾아서 반환한다.
return new PathMatchingResourcePatternResolver(classLoader)
.getResources("classpath*:" + target + ".properties");
}
catch (Exception ex) {
return NO_RESOURCES;
}
}
클래스패스 내에 basename에 해당하는 properties 파일이 있는지 찾아서 반환한다.
🔽 여기서 알 수 있는 중요한 사실 2 가지가 있다 🔽
클래스패스 내에 basename에 해당하는 properties 파일이 없다면 자동 설정이 안된다. 따라서 사용하지 않더라도 messages.properties와 같은 기본 파일은 만들어두는 게 좋을 듯 하다.
메세지파일을 yml 형식으로 관리하는 건 불가하다. 현재 Spring Boot에서 공식적으로 지원하는 건 properties 형식이다.
yml로 관리하기 위한 라이브러리가 있긴하지만, 관련 문서가 적고 에러가 나도 해결하기 까다로울 듯 하다.
messageSourceAutoConfiguration.java
에서 아래 코드 부분은 MessageSource
의 속성을 설정하는 부분이다.
@Bean
@ConfigurationProperties(prefix = "spring.messages")
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
@ConfigurationProperties
어노테이션은 application.properties
나 application.yml
파일에서 prefix로 시작하는 속성을 찾아서 객체에 바인딩 해주는 역할을 한다. (@Value
어노테이션과 비슷한 역할이지만, 여러 속성을 한 번에 그리고 유연하게 바인딩할 수 있다.)
@ConfigurationProperties(prefix = "spring.messages")
이기 때문에, 위에서 작성한 예시처럼 application.properties나 application.yml 에 spring.messages의 속성을 설정할 수 있는 것이다.
messageSourceAutoConfiguration.java
에서 아래 코드 부분은 Bean
으로 등록할 MessageSource를 만드는 부분이다.
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
if (StringUtils.hasText(properties.getBasename())) {
messageSource.setBasenames(StringUtils
.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
}
if (properties.getEncoding() != null) {
messageSource.setDefaultEncoding(properties.getEncoding().name());
}
messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
Duration cacheDuration = properties.getCacheDuration();
if (cacheDuration != null) {
messageSource.setCacheMillis(cacheDuration.toMillis());
}
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
return messageSource;
}
새로운 MessageSource를 만들고 위에서 만든 MessageSourceProperties
의 값들을 MessageSource에 세팅한다.
마지막으로 한 가지 질문에 대해 더 살펴보려고 한다.
설정 파일에 MessageSource도 만들고, application.properties에 자동설정 세팅도 해놓으면 어떤 메세지를 선택할까?
@Configuration
public class MessageConfig {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("customMessage/messages");
messageSource.setFallbackToSystemLocale(false);
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
}
spring.messages.basename=message/messages
spring.messages.encoding=UTF-8
spring.messages.fallbackToSystemLocale=false
@RequiredArgsConstructor
@RestController
public class testController {
private final MessageSource messageSource;
@GetMapping()
public ResponseEntity<String> printHello() {
return ResponseEntity.status(200).body(messageSource.getMessage("hello", null, LocaleContextHolder.getLocale()));
}
}
테스트를 해봤더니, 커스텀한 MessageSource의 메세지를 가져오는 걸 확인할 수 있었다.
그 이유는 MessageSourceAutoConfiguration
에 있는 @ConditionalOnMissingBean
어노테이션 때문이다.
해당 어노테이션은 특정 bean이 없어야 조건을 만족시키는 어노테이션인데, MessageSource를 직접 bean으로 만들어버리면 저 조건을 만족시키지 못한다.
즉 MessageSource bean을 직접 만들지 않을 때에만 자동설정된 MessageSource bean을 사용할 수 있다.
@AutoConfiguration
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {...}