개요
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; | |
} |
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"); | |
} | |
} |
* 코드로 예시를 보여드렸지만 제 경험상 코드로 불러오는 경우는 거의 없고 타임리프에 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편 (김영한 강사님)
'Spring' 카테고리의 다른 글
[SpringBoot] Validation 간단 정리 - 2 (Bean Validation) (0) | 2021.07.25 |
---|---|
[SpringBoot] Validation 간단 정리 - 1 (BindingResult, Validator) (6) | 2021.07.16 |
HttpMessageConverter 간단 정리 (0) | 2021.06.11 |
Spring MVC 구조 정리 (0) | 2021.06.09 |
클라이언트에서 서버로 HTTP 요청 메시지 보내는 방법 (0) | 2021.06.02 |