스프링 MVC-스프링 메세지, 국제화

스프링 메세지

보통 프로그램을 구축할 때 기획단계에서부터 단어를 통일해서 사용하는 경우가 많다. ‘상품 등록’이 ‘상품 저장’, ‘상품 추가’ 와 같이 다른 단어와 혼용된다면 어떤 기능을 하는지도 불분명하고 유지보수가 어렵기 때문이다.
그런데 만약 ‘상품 등록’을 기획에서 ‘상품 등재’로 바꾸기로 했다면, 하드 코딩으로 문구를 입력했을 경우 우리는 모든 파일을 다 뒤져가며 문구를 바꿔주어야 한다.
이러한 사태를 방지하기 위해 스프링에서는 메세지 기능을 지원하는데, 어떠한 기능인지 하나씩 알아보자.

메세지 소스

스프링에서 메세지 기능을 사용하기 위해서는 스프링이 제공하는 MessageSource를 스프링 빈으로 등록하여 사용하면 되는데, MessageSource는 인터페이스이다.
따라서 보통 구현체인 ResourceBundleMessageSource를 스프링 빈으로 등록하여 사용하게 된다.

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

스프링 빈으로 등록하여 사용하는 코드이다. setBasenames()로 기본 이름을 지정할 수 있는데, messages, errors 두 가지를 지정했으므로 messages.properties, errors.properties 두 파일을 읽어서 메세지를 사용하게 된다.
setDefaultEncoding()으로는 인코딩 정보를 지정하는데 보통은 utf-8로 지정한다.

하지만 이렇게 빈을 직접 등록할 필요 없이 스프링부트를 사용한다면 MessageSource가 자동으로 빈으로 등록되어있다! 위에서 한 basename과 인코딩 지정은 application.properties 파일에서
spring.messages.basename=messages,config.i18n.messages
와 같이 작성하여 입력할 수 있다. 스프링부트의 기본 basename은 messages이므로 별다른 설정이 없다면 messages.properties 파일을 읽어 사용한다.

메세지 파일

위에서 설명한 대로 messages.properties 파일을 작성해보자. 이후 국제화 기능도 사용하기 위해 비슷한 내용으로 messages_en.properties 파일도 작성하였다.
이렇게 뒤에 나라 이름을 붙여 파일을 작성하면 후에 국제화 기능을 사용할 때 나라에 맞게 자동으로 메세지를 갈아끼워준다.
messages.properties
hello=안녕
hello.name=안녕 {0}

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

메세지 사용

  
package hello.itemservice.message;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;

import java.util.Locale;

import static org.assertj.core.api.Assertions.*;

@SpringBootTest
public class MessageSourceTest {

    @Autowired
    MessageSource ms;

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

    }

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

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

    }
}

여러 메세지 기능들을 사용해본 테스트 코드이다. 기본적으로 빈으로 등록된 MessageSource를 @Autowired를 통해 불러와 MessageSouerce의 getMessage() 메서드를 사용하여 기능을 이용한다.
파라미터는 각각 (code, args, default, locale) 순이다.
code는 메세지 파일에서 읽어올 이름을 뜻한다. 위의 코드에서는 hello를 입력하여 messages.properties에서 hello=안녕 을 읽어왔다.
args는 매개변수를 뜻한다. argumentMessage 메서드를 보면 매개변수가 있는 메세지인 hello.name에 매개변수로 “Spring”을 넣은 것을 볼 수 있다.
따라서 hello.name=안녕 {0}에서 매개변수가 들어가 안녕 Spring 이 출력되는 것을 확인할 수 있다.
default는 있어도 없어도 되는 파라미터로 기본 메세지를 뜻한다. 찾는 코드가 없을 때 default 부분이 없다면 NoSuchMessageException 예외를 반환하지만 default 부분이 있다면 기본 메세지를 반환한다.
locale은 국가 정보를 뜻한다. locale 정보가 없다면 Locale.getDefault()를 호출하여 시스템의 기본 로케일을 사용한다.
따라서 locale을 null로 두고 호출하면 getDefault()로 messages_ko.properties를 호출 -> 없으니 기본 메세지인 messages.properties 사용 과 같은 흐름을 따라간다.
locale에 Locale.ENGLISH를 넣어주면 messages_en.properties가 사용되는 것을 확인할 수 있다.

웹에 메세지 적용

타임리프에서는 메세지 표현식 ’#{}’를 사용하여 편리하게 메세지를 조회할 수 있다. 예를 들어 위의 코드처럼 hello라는 코드의 메세지를 조회하려면 ‘#{hello}’라고 작성하면 된다.
messages.properties에 적절한 메세지들을 추가한 뒤 원래 사용하던 상품 관리 폼의 text들을 메세지들로 치환시켜 보자.

  
<!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>

기존에 그냥 text로 사용했던 부분들이 ‘th:text=”#{label.item.price}”‘와 같이 메세지 기능을 이용하도록 수정된 모습이다.
해당 코드를 실행시키면 이전과 같이 작동하고, messages.properties 파일의 메세지를 변경하면 해당 메세지를 사용한 부분이 모두 같이 바뀌는 것을 확인할 수 있다.
만약 파라미터를 사용하고 싶다면 타임리프의 변수 표현식을 사용하여
‘th:text=”#{hello.name(${item.itemName})}”’ 와 같이 사용할 수 있다.

이번엔 국제화 기능을 사용하여 메세지들을 영어로 바꿔보자. 무엇을 해야할까?
우리는 해당 국가에 맞는 messages_en.properties와 같은 파일들만 작성해주면 된다. 이후에는 스프링의 LocaleResolver에서 HTTP의 Accept-Language 등을 확인하여 적절한 국가의 메세지 파일을 읽어 메세지를 치환해준다.
국가마다 파일을 작성한 후 브라우저의 국가 설정을 변경, 위의 수정한 웹을 띄워보면 메세지를 사용한 부분이 해당 국가의 언어로 변경된 것을 확인할 수 있다.

스프링의 메세지 기능에 대해 알아보았다. 필수적이진 않지만 알아두면 매우 유용한 기능인 것 같다.
다음 포스팅에서는 드디어 중요한 부분인데, 검증에 대해 알아보고, 이후엔 로그인, 세션, 쿠키 등 웹 구축에 필수적인 부분들에 대해 배우고 포스팅 할 예정이다.