Spring

[Spring Boot] 메시지, 국제화 간단 정리

꾸준함. 2021. 7. 10. 22:57

개요

HTML에 문구를 하드 코딩해버리면 기존 문구를 일괄적으로 변경해야 할 때 혹은 한글을 모르는 외국인들에게도 서비스를 제공하고 싶을 때 일일이 변경해야 하기 때문에 상당히 곤란한 상황에 직면하게 됩니다.

따라서, HTML에 하드코딩하는 대신 다양한 메시지를 한 곳에서 관리하는 메시지 기능을 익혀놓으면 실무에서 유용하게 사용할 수 있습니다.

 

메시지 기능 설정

메시지 기능을 사용하기 위해서는 MessageSource라는 스프링 빈을 등록해주면 되고 이는 인터페이스이기 때문에 구현체인 ResourceBundleMessageSource 빈을 등록해주면 됩니다. (스프링 부트에서는 자동으로 등록)

 

@Bean
public MessageSource messageSource() {
	ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    messageSource.setBasenames("messages", "[원하는 파일명]");
    messageSource.setDefaultEncoding("utf-8");
    
    return messageSource;
}

* basenames: 설정 파일의 이름 등록 (messages로 지정하면 messages.properties 파일을 읽어서 사용)

* 국제화 기능 적용을 위해서는 messages_en.properties와 같이 파일명 마지막에 언어 정보 추가 (국제화 파일이 없을 경우 디폴트인 message.properties 사용)

* 여러 파일을 한번에 지정할 수 있으며 파일명도 원하는 대로 가능하지만 통상적으로 messages 사용 (스프링 부트의 디폴트 파일명)

*  Spring Boot에서는 src/main/resources/messages.properties를 찾았을 때 자동으로 MessageSource 빈을 등록

 

MessageSource 인터페이스 분석

 

public interface MessageSource {
/**
* Try to resolve the message. Return default message if no message was found.
* @param code the message code to look up, e.g. 'calculator.noRateSet'.
* MessageSource users are encouraged to base message names on qualified class
* or package names, avoiding potential conflicts and ensuring maximum clarity.
* @param args an array of arguments that will be filled in for params within
* the message (params look like "{0}", "{1,date}", "{2,time}" within a message),
* or {@code null} if none
* @param defaultMessage a default message to return if the lookup fails
* @param locale the locale in which to do the lookup
* @return the resolved message if the lookup was successful, otherwise
* the default message passed as a parameter (which may be {@code null})
* @see #getMessage(MessageSourceResolvable, Locale)
* @see java.text.MessageFormat
*/
@Nullable
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
/**
* Try to resolve the message. Treat as an error if the message can't be found.
* @param code the message code to look up, e.g. 'calculator.noRateSet'.
* MessageSource users are encouraged to base message names on qualified class
* or package names, avoiding potential conflicts and ensuring maximum clarity.
* @param args an array of arguments that will be filled in for params within
* the message (params look like "{0}", "{1,date}", "{2,time}" within a message),
* or {@code null} if none
* @param locale the locale in which to do the lookup
* @return the resolved message (never {@code null})
* @throws NoSuchMessageException if no corresponding message was found
* @see #getMessage(MessageSourceResolvable, Locale)
* @see java.text.MessageFormat
*/
String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
/**
* Try to resolve the message using all the attributes contained within the
* {@code MessageSourceResolvable} argument that was passed in.
* <p>NOTE: We must throw a {@code NoSuchMessageException} on this method
* since at the time of calling this method we aren't able to determine if the
* {@code defaultMessage} property of the resolvable is {@code null} or not.
* @param resolvable the value object storing attributes required to resolve a message
* (may include a default message)
* @param locale the locale in which to do the lookup
* @return the resolved message (never {@code null} since even a
* {@code MessageSourceResolvable}-provided default message needs to be non-null)
* @throws NoSuchMessageException if no corresponding message was found
* (and no default message was provided by the {@code MessageSourceResolvable})
* @see MessageSourceResolvable#getCodes()
* @see MessageSourceResolvable#getArguments()
* @see MessageSourceResolvable#getDefaultMessage()
* @see java.text.MessageFormat
*/
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}
view raw .java hosted with ❤ by GitHub

 

 

MessageSource 인터페이스를 확인하면 messages.properties에서 메시지를 읽어오는 기능을 제공하는 것을 확인할 수 있습니다.

또한, 국제화 기능을 사용할 때는 HTTP accept-language 헤더 값 혹은 locale 정보를 기반으로 국제화 파일을 선택하는데 Locale이 en_US의 경우 messages_en_US.properties 파일을 찾고 없을 경우 messages_en.properties 파일을 찾아보고 그래도 없으면 디폴트 파일인 messages.properties 파일에서 찾습니다.

 

간단하게 몇 가지 예시를 들어보겠습니다.

 

messages.properties

 

hi=안녕
hi.nickname=안녕 {0}

 

messages_en.properties

 

hi=hi
hi.nickname=hi {0}

 

예시 코드

 

public class ExampleTest {
@Autowired
MessageSource messageSource;
// code: hi
// args: null
// locale: null (basename에서 설정한 기본 이름 메시지 파일 조회 -> messages.properties)
@Test
void basicExample() {
String result = messageSource.getMessage("hi", null, null);
Assertions.assertThat(result).isEqualTo("안녕");
}
// 메시지가 존재하지 않을 경우 NoSuchMessageException 발생
@Test
void noSuchMessageExceptionExample() {
Assertions.assertThatThrownBy(() ->
messageSource.getMessage("no_code", null, null))
.isInstanceOf(NoSuchMessageException.class);
}
// 메시지가 존재하지 않을 경우 디폴트 메시지 전달
@Test
void defaultMessageExample() {
String result = messageSource.getMessage("no_code", null, "디폴트 메시지", null);
Assertions.assertThat(result).isEqualTo("디폴트 메시지");
}
// 메시지에 매개변수를 전달하는 경우 hi {0} -> hi Spring
@Test
void messageArgumentExample() {
String message = messageSource.getMessage("hi.nickname", new Object[]{"Spring"}, null);
Assertions.assertThat(message).isEqualTo("안녕 Spring");
}
// 첫 번째는 locale 정보가 없으므로 messages.properties 사용
// 두 번째는 locale 정보가 있으나 messages_ko.properties 파일이 없으므로 messages.properties 파일 사용
@Test
void defaultLanguageExample() {
Assertions.assertThat(messageSource.getMessage("hi", null, null)).isEqualTo("안녕");
Assertions.assertThat(messageSource.getMessage("hi", null, Locale.KOREA)).isEqualTo("안녕");
}
// locale 정보가 있고 Locale.ENGLISH이므로 messages_en.properties 파일 사용
@Test
void englishLanguageExample() {
Assertions.assertThat(messageSource.getMessage("hi", null, Locale.ENGLISH)).isEqualTo("hi");
}
}
view raw .java hosted with ❤ by GitHub

 

 

* 코드로 예시를 보여드렸지만 제 경험상 코드로 불러오는 경우는 거의 없고 타임리프에 th:text 기능을 통해 직접 지정하는 경우가 대다수였습니다.

 

스프링 언어 선택 방법

스프링은 앞선 예시처럼 Locale 값을 보고 언어를 선택하기도 하지만 대부분 HttpServletRequest의 Accept-Language 헤더 값을 보고 언어를 선택합니다.

스프링은 Locale 선택 방식을 변경할 수 있도록 LocaleResolver 인터페이스를 제공하며 스프링 부트는 기본적으로 Accept-Language를 활용하기 때문에 AcceptHeaderLocaleResolver를 디폴트로 사용합니다.

 

Accept-Language가 하나일 경우에는 간단하지만 상황에 따라 아래와 같이 복잡한 상황이 발생하기도 합니다.

  • 원하는 상황: 가급적 한국어를 원하지만 불가능한 경우 영어 그 것마저 불가능하다면 불어
  • 해결책: 이런 경우에는 Accept-Language 헤더에 우선순위인 Quality Values 값을 사용해야 합니다.
    • 값이 클수록 우선순위가 높은 것이며 범위는 0 ~ 1 (생략할 경우 1)
    • 원하는 상황에 맞는 예시: Accept-Language: ko-KR; ko;q=0.9, en-US;q=0.8, en;q=0.8, fr;q=0.3

 

메시지 기능을 사용하면서 아쉬웠던 부분

메시지 파일이 하나의 공통 파일이다 보니 다수의 개발자가 여러 브랜치에서 나누어 작업하고 코드를 병합할 때 매번 messages.properties와 messages_en.properties 충돌이 나 merge conflict를 해결하는 번거로움이 발생하고 있습니다.

위 문제를 해결하기 위해서는 파트별로 messages.propreties를 분리해야 할 것 같은데 개발기간이 타이트한 지금으로서는 해결하기 어려운 문제처럼 보입니다.

이러한 아쉬움이 있더라도 HTML 하드코딩보다는 훨씬 괜찮은 방식인 것은 확실합니다! 

 

출처

인프런 스프링 MVC 2편 (김영한 강사님)

 

 

 

 

 

반응형