BackEnd/Spring MVC

[Spring] 메시지, 국제화

샤아이인 2022. 3. 5.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

 

1. 스프링 메시지 소스 설정

스프링은 기본적으로 메시지 관리 기능을 제공한다.

 

메시지 관리 기능을 사용하려면 스프링이 제공하는 MessageSource를 스프링 빈으로 등록하면 되는데, MessageSource는 인터페이스다.

따라서 구현체인 ResourceBundleMessageSource 를 스프링 빈으로 등록하면 된다.

 

직접 등록하는 방식을 코드로 살펴보자.

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

- setBasenames() 로 설정 파일의 이름을 지정할 수 있다.

"messages" 로 지정하면 messages.properties 파일을 읽어서 사용하게 됩니다.

 

추가로 국제화 기능을 적용하려면 messages_en.properties , messages_ko.properties 와 같이 파일명 마지막에 언어 정보를 주면된다. 만약 찾을 수 있는 국제화 파일이 없으면 messages.properties를 기본으로 사용하게 된다.

 

위의 코드에서 사용한것 처럼 여러 파일은 쉼표로 구분하여 한번에 지정할수도 있다.

 

- setDefaultEncoding()을 통하여 인코딩 정보를 지정합니다. 일반적으로 UTF-8을 사용합니다.

 

● 스프링 부트

스프링 부트를 사용하면 스프링 부트가 MessageSource 를 자동으로 스프링 빈으로 등록합니다.

또한 스프링 부트에서는 메시지 소스를 자동으로 설정해주기 까지 합니다!

 

기본은 spring.messages.basename=messages 이지만, 만약 사용자가 직접 지정하고 싶다면 application.properties에 등록하면 된다.

 

MessageSource를 스프링 빈으로 등록하지 않고, 스프링 부트와 관련된 별도의 설정을 하지 않으면 messages 라는 이름으로 기본 등록된다. 따라서 messages_en.properties , messages_ko.properties , messages.properties 파일만 등록하면 자동으로 인식된다.

 

다음 섹션에서 메시지 소스를 사용하기 위해 messages.properties, messages_en.properties 를 만들자.

// messages.properties
hello=안녕 
hello.name=안녕 {0}
 
// messages_en.properties
hello=hello
hello.name=hello {0}
 

 

2. 스프링 메시지 소스 사용

MessgaeSource 인터페이스를 보면 코드를 포함한 일부 파라미터로 메시지를 읽어오는 기능을 제공합니다.

 

다음 테스트 코드를 살펴봅시다.

@SpringBootTest
public class MessageSourceTest {
    @Autowired MessageSource ms;

    @Test
    void helloMessage() {
        String result = ms.getMessage("hello", null, null); 
        assertThat(result).isEqualTo("안녕");
    } 
}
 

MessageSource 는 자동주입을 통하여 빈을 받아왔습니다. 이를 사용하는 다음 코드를 살펴볼까요?

ms.getMessage("hello", null, null);
 

총 3개의 인자를 넘겼는데, 각각 code, args, locale에 해당합니다.

일단 가장 간단하게 "hello"만 code로 넘기고, 나머지는 null을 넘겨주었습니다.

 

locale 정보가 없으면 basename 에서 설정한 기본 이름 메시지 파일을 조회한다. basename 으로 messages 를 지정 했으므로 messages.properties 파일에서 데이터 조회한다.

 

● MessageSourceTest 추가 - 메시지가 없는 경우, 기본 메시지

@Test
void notFoundMessageCode(){
    assertThatThrownBy(() -> ms.getMessage("no_code", null, null))
            .isInstanceOf(NoSuchMessageException.class);
}

@Test
void notFoundMessageCodeDefaultMessage(){
    String result = ms.getMessage("no_code", null, "기본 메시지", null);
    assertThat(result).isEqualTo("기본 메시지");
}
 

notFoundMessageCode() 의 code로는 "no_code"를 넘겨주었는데, 이는 messages.properties에 등록하지 않은 code이다.

따라서 NoSuchMessageException이 발생하며, 이를 테스트 코드로 작성한것 이다.

 

두번째 테스트 코드는 이전과 같지만, default값을 넘겨주었을 뿐 이다.

따라서 인자로 넘긴 "기본 메시지"가 전달되어 이 값이 출력된다.

 

● MessageSourceTest추가 - 국제화 파일 선택1

@Test
void argumentsMessage() {
    String message = ms.getMessage("hello.name", new Object[]{"Spring"}, null);
    assertThat(message).isEqualTo("안녕 Spring");
}
 

messages.properties에서 다음과 같이 작성한적이 있다.

hello.name=hello {0}
 

{0}에 해당하는 부분에 배열로 "Spring"이라는 단어를 전달하였다.

Object배열 index 0번째 칸에는 "Spring"이 들어있는데, 이를 꺼내오기 위해 {0}으로 0번을 지정한것 이다.

결과로는 hello Spring 이 나오게 된다.

 

● 국제화 파일 선택

국제화를 통하여 언어를 변경할수가 있다. 이는 locale 정보를 기반으로 선택하게 된다.

Locale이 en_US 의 경우 messages_en_US => messages_en => messages 순서로 찾는다.

구체적인것 부터 먼저 찾아보다 없으면 디폴트를 사용하는 것 이다.

 

테스트 코드를 통해서 확인해 보자.

@Test
void defaultLang() {
    assertThat(ms.getMessage("hello", null, null)).isEqualTo("안녕");
    assertThat(ms.getMessage("hello", null, Locale.KOREA)).isEqualTo("안녕");
}
 

- ms.getMessage("hello", null, null) : locale 정보가 없으므로 messages를 사용한다.

- ms.getMessage("hello", null, Locale.KOREA) : locale 정보가 있지만, message_ko가 없으므로 messages 를 사용한다.

 

다음 테크트 코드는 영어를 Locale로 설정한 경우이다.

@Test
void enLang() {
    assertThat(ms.getMessage("hello", null, Locale.ENGLISH)).isEqualTo("hello");
}
 

- ms.getMessage("hello", null, Locale.ENGLISH) : locale 정보가 Locale.ENGLISH이므로 messages_en 을 찾아서 사용한다.

 

3. 웹 애플리케이션에 메시지 적용하기

실제 웹 어플리케이션에 메시지를 적용해 보자.

messages.properties 는 다음과 같다.

label.item=상품
label.item.id=상품 ID
label.item.itemName=상품명
label.item.price=가격
label.item.quantity=수량

page.items=상품 목록
page.item=상품 상세
page.addItem=상품 등록
page.updateItem=상품 수정

button.save=저장
button.cancel=취소
 

● 타임리프 메시지 적용

타임리프의 메시지 표현식 #{...} 를 사용하면 스프링의 메시지를 편리하게 조회할 수 있다.

예를 들어서 방금 등록한 상품이라는 이름을 조회하려면 #{label.item} 이라고 하면 된다.

 

- 렌더링 전

<div th:text="#{label.item}"></h2>
 

- 렌더링 후

<div>상품</h2>
 

 

● addForm 수정

우선 수정된 addForm의 코드를 살펴보자.

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container {
            max-width: 560px;
        }
    </style>
</head>
<body>

<div class="container">

    <div class="py-5 text-center">
        <h2 th:text="#{page.addItem}">상품 등록 폼</h2>
    </div>

    <form action="item.html" th:action th:object="${item}" method="post">
        <div>
            <label for="itemName" th:text="#{label.item.itemName}">상품명</label>
            <input type="text" id="itemName" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
        </div>
        <div>
            <label for="price" th:text="#{label.item.price}">가격</label>
            <input type="text" id="price" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요">
        </div>
        <div>
            <label for="quantity" th:text="#{label.item.quantity}">수량</label>
            <input type="text" id="quantity" th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
        </div>

        <hr class="my-4">

        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit" th:text="#{button.save}">상품 등록</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='items.html'"
                        th:onclick="|location.href='@{/message/items}'|"
                        type="button" th:text="#{button.cancel}">취소</button>
            </div>
        </div>

    </form>

</div> <!-- /container -->
</body>
</html>
 

 

- 페이지 이름 적용

<h2 th:text="#{page.addItem}">상품 등록</h2>
 

 

- 레이블 적용

<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
<label for="price" th:text="#{label.item.price}">가격</label>
<label for="quantity" th:text="#{label.item.quantity}">수량</label>
 

 

- 버튼 적용

<button type="submit" th:text="#{button.save}">저장</button>
<button type="button" th:text="#{button.cancel}">취소</button>
 

 

이후 messages.properties 파일을 다음과 같이 수정하면 addForm 화면에서도 변경되는것을 확인할 수 있다.

label.item=상품22
label.item.id=상품 ID22
label.item.itemName=상품명22
label.item.price=가격22
label.item.quantity=수량22

page.items=상품 목록22
page.item=상품 상세22
page.addItem=상품 등록22
page.updateItem=상품 수정22

button.save=저장22
button.cancel=취소22
 

결과는 다음과 같다.

설정파일에서 변경한것 처럼 22가 모두 추가된것을 확인할 수 있다.

 

● 파라미터 적용하기

다음과 같이 사용하면 된다.

<p th:text="#{hello.name(${item.itemName})}"></p>
 

4. 웹 애플리케이션에 국제화 적용하기

이미 직전에 템플릿 파일에서 #{...} 을 통해 메시지를 사용하도록 적용했기 때문에 이후부터는 설정파일만 추가해주면 국제화가 가능하다.

 

messages_en.properties

label.item=Item
label.item.id=Item ID
label.item.itemName=Item Name
label.item.price=price
label.item.quantity=quantity

page.items=Item List
page.item=Item Detail
page.addItem=Item Add
page.updateItem=Item Update

button.save=Save
button.cancel=Cancel
 

● 스프링의 국제화 메시지 선택

앞서 MessageSource 테스트에서 보았듯이 메시지 기능은 Locale 정보를 알아야 언어를 선택할 수 있다.

결국 스프링도 Locale 정보를 알아야 언어를 선택할 수 있는데, 스프링은 언어 선택시 기본으로 Accept-Language 헤더의 값을 사용한다.

 

● LocaleResolver

스프링은 Locale 선택 방식을 변경할 수 있도록 LocaleResolver 라는 인터페이스를 제공하는데, 스프링 부트는 기본으로 Accept-Language 를 활용하는 AcceptHeaderLocaleResolver 를 사용한다.

public interface LocaleResolver {
    Locale resolveLocale(HttpServletRequest request);

    void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);
}
 

Locale의 선택방식을 변경하려면 LocaleResolver의 구현체를 변경해서 쿠키나 세션 기반의 Locale선택 기능을 사용할 수 있다.

댓글