메시지, 국제화

김회민·2023년 2월 19일
0

Spring

목록 보기
8/25

메시지, 국제화란?

어느날, 신나게 개발을 진행하다가 화면에 보이는 문구가 마음에 들지않고 자꾸만 신경쓰일 때가 있다. 그런데 그 문구는 전부 코드안에 때려박혀있는 데다가(하드코딩) 동일한 문구가 한 두군데가 아니어서 쉽사리 건들기도 어렵거니와 까딱하면 일부만 수정되는 경우가 생길 수도 있다.

그래서 이 현상을 방지하고자, Spring에서는 그런 문구를 따로 관리할 수 있도록 기능을 제공해주는데, 이를 메시지 기능이라고 한다. 또한, 이것을 확장하여 현재 접속하고 있는 국가에 따라 (Locale 정보) 다른 화면을 보여줄 수 있는 기능 또한 제공해주는데, 이를 국제화 기능이라고 한다.

정리

  • 메시지: 화면에 나오는 동일한 문구(문자열)를 하나의 파일에서 관리하는 것.
  • 국제화: 클라이언트가 접속한 국가 정보(Locale 정보)에 따라서 다른 문구를 보여주기 위한 기능.

설정

Spring Bean 등록

@Configuration
public class MessageConfig {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasenames("messages", "errors");
        messageSource.setDefaultEncoding("utf-8");
        return messageSource;
    }
}

Spring Boot - application.properties

공식 Docs

# 메시지 파일이 있는 경로 + 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})를 사용하지 않을지를 설정
    • true: 인수 사용 안함, false: 인수 사용
    • 기본 설정값: false
  • spring.messages.cache-duration
    • 메시지 번들의 캐싱 주기 설정
    • 따로 설정하지 않으면 영구히 캐싱된다.
    • 기간 접미사(h,m,s)를 설정하지 않으면 기본적으로 초(s)를 사용한다.
    • 기본 설정값: forever
  • spring.messages.fallback-to-system-locale
    • 특정 Locale에 대한 파일을 찾지 못한 경우, System Locale을 사용할지 설정
    • false로 설정하면, 무조건 messages.properties를 찾는다.
    • 기본 설정값: true
  • spring.messages.use-code-as-default-message
    • 메시지 파일을 찾지 못한 경우, NoSuchMessageException 대신 메시지 코드를 반환할지 설정
    • 공식 문서에 따르면 운영 환경에서 true를 사용하지 말라고 이야기한다.
    • 기본 설정값: 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

MessageSource Interface

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 를 주입하여 사용된다.

getMessage 메서드

  • String code
    • 메시지 파일에 작성된 key를 입력한다.
  • Object[] args
    • 메시지 인수({0}, {1})에 들어갈 메시지들을 넣는다.
    • new Object[]{"...", "..."}
  • String defaultMessage
    • 첫 번째 파라미터인 code를 찾은 결과, 찾지 못했다면 출력될 메시지를 입력한다.
  • Locale locale
    • 찾고 싶은 지역을 입력한다.
    • null을 전달할 수는 있지만 권장사항이 아니며(@NotNull), null을 전달한 경우 Locale.getDefault()를 호출해 Spring이 실행되고 있는 환경의 Locale를 바탕으로 검색한다.
    • 만약, 설정에서 spring.messages.fallback-to-system-locale 을 false로 설정한 경우 messages.properties를 바로 찾는다. 이마저도 없으면 NoSuchMessageException이 발생한다.
    • Spring의 DispatcherServlet은 ArgumentResolver를 통해 Controller에 클라이언트의 Locale 정보를 줄 수 있는데, 이를 이용해 MessageSource.getMessage에 Locale 값을 전달할 수 있다.

메시지 파일을 찾는 방법

Locale값에 따라 messages_* 파일을 검색하는데, 구체적인 것부터 기본까지 차례대로 검색한다.

예를 들어서 Locale값으로 EN_US가 왔다고 가정해보자.

  1. messages_en_US.properties
  2. messages_en.properties
  3. messages.properties
  4. NoSuchMessageException

순으로 파일을 검색하게 된다.

메시지 파일 작성하기

# messages.properties
hello      = 안녕
hello.name = 안녕 {0}

# messages_en.properties
hello      = hello
hello.name = hello {0}

properties 파일의 특성상, key = value 형식으로 작성하면 된다.

MessageSource.getMessagecode 파라미터는 key값을 토대로 찾는다.

value{0}, {1} 파라미터를 사용하면, Object[] args 파라미터에 입력된 순서대로 매핑을 시킬 수 있다.

메시지 파일 사용하기

전체 소스 코드 경로(깃헙)

Spring

@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");
    }
}

Thymeleaf

<!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 정보도 자동으로 넘어간다.

profile
백엔드 개발자 지망생

0개의 댓글