공부/Spring

메시지, 국제화

Stair 2025. 3. 30. 16:31
반응형

메시지, 국제화 소개

 

메시지

만약 기획자가 화면에 보이는 문구가 마음에 들지 않는다고, '상품명'이라는 단어를 모두 '상품이름'으로 고쳐달라고 하면, 어떻게 해야할까?

여러 화면에 보이는 상품명, 가격, 수량 등, label에 있는 단어를 변경하려면 '상품명'이라는 단어가 존재하는 화면을 다 찾아가면서 모두 변경해야 한다. 지금처럼 화면 수가 적으면 문제가 되지 않지만, 화면이 수십개 이상이라면, 수십개의 파일을 모두 고쳐야 한다.

- addForm.html, editForm.html, item.html, items.html

 

왜냐하면 해당 HTML 파일에 메시지가 하드코딩 되어있기 때문이다.

 

이런 다양한 메시지를 한 곳에서 관리하도록 하는 기능을 메시지 기능이라 한다.

 

ex) message.properties라는 메시지 관리용 파일을 만든다.

item = 상품

item.id = 상품 ID

item.itemName = 상품명 

등등

 

각 HTML들은 다음과 같이 해당 데이터를 key 값으로 불러서 사용한다.

<label for = "itemName" th:text="#{item.itemName></label>

 

<label for = "id" th:text="#{item.id></label>

 

 

국제화

메시지에서 한발 더 나아가보자.

메시지에서 설명한 메시지 파일(message.properties)를 각 나라별로 별도로 관리한다면, 서비스를 국제화 할 수 있다.

 

ex)messages_en.properties

item = item

item.itemName= Item Name

item.price = price

 

ex)messages_ko.properties

item = 상품

item.id = 상품 ID

item.itemName = 상품명

--> 영어를 사용하는 사람이면 messages_en.propertis를 사용하고, 한국어를 사용하는 사람이면 messages_ko.propertis를 사용하게 개발하면 된다.

이렇게 하면 사이트를 국제화 할 수 있다.

 

 

 

스프링 메시지 소스 설정

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

 

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

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

 

- basenames : 설정 파일의 이름을 저장한다.

    - messages로 지정하면, messages.properties 파일을 읽어서 사용한다.

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

    - 파일의 위치는 /resources/messages.properties에 두면 된다.

    - 여러 파일을 한번에 지정할 수 있다. 여기서는 messages, errors 둘을 지정했다.

- defaultEncoding : 인코딩 정보를 지정한다. utf-8을 사용한다.

 

 

---> 스프링 부트를 사용하면 스프링 부트가 알아서 MessageSource를 자동으로 스프링 빈에 등록한다.

 

 

스프링 부트 메시지 소스 설정

스프링 부트를 사용하면 다음과 같이 메시지 소스를 설정할 수 있다.

application.propertis

spring.messages.basename=messages,config.i18n.messages

 

스프링 부트 메시지 소스 기본 값

spring.message.basename=messages

 

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

 

 

 

메시지 파일 만들기

메시지 파일을 만들어보자. 국제화 테스트를 위해 messages_en파일도 추가하자. resources 디렉토리 밑에 파일을 만든다.

 

- messages.properties : 기본값으로 사용(한글)

hello= 안녕
hello.name = 안녕 {0}

- messages_en.properties : 영어 국제화 사용

hello= hello
hello.name = hello {0}

 

properties를 두개 만들면 자동으로 분류되어 묶여진다.

 

 

이제 스프링 메시지 소스를 사용해보자.

 

**중요 : 한글 깨짐이 발생할 수 있다.

한글을 사용하였기 때문에 test에도 한글을 사용할 예정인데,  인텔리제이에서 한글이 깨져 테스트 코드가 제대로 동작하지 않을수도 있다. 이 문제를 다음처럼 해결하도록 하자.

1. setting - File Encodings로 이동하자.

2. default encoding for properties files를 ISO-8859-1에서 UTF-8로 변경하자.

3. Transparent native-to-ascii conversion을 체크하자

 

이제 스프링이 제공하는 메세지 소스를 어떻게 사용하는지 테스트 코드를 통해서 학습해보자.

@SpringBootTest
public class MessageSourceTest {

    @Autowired
    MessageSource ms;

    @Test
    public void helloMessage() throws Exception{
        String result = ms.getMessage("hello", null, null);
//        String result = ms.getMessage("hello", null, Locale.KOREA);
        assertThat(result).isEqualTo("안녕");
    }

 

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

    - code : hello

    - args : null

    - locale : null

 

가장 단순한 테스트는 메시지 코드로 hello를 입력하고, 나머지 값은 null을 입력하였다.

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

 

메시지가 없는 경우 & 기본 메시지

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

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

- 메시지가 없는 경우에는 NoSuchMessageException이 발생한다.

- 메시지가 없어도 기본 메시지(defaultMessage)를 사용하면 기본 메시지가 반환된다.

 

매개변수를 사용

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

다음 메시지의 {0} 부분은 매개변수를 전달해서 치환할 수 있다.

hello.name=안녕 {0} -> Spring 단어를 매개변수로 전달 -> 안녕 Spring

 

국제화 파일 선택

locale 정보를 기반으로 국제화 파일을 선택할수도 있다.

- Locale이 en_US의 경우 message_en_US -> messages_en -> message 순서로 찾는다

- Locale에 맞추어 구체적인 것이 있으면, 구체적인 것을 찾고, 없으면 디폴트를 찾는다고 이해하면 된다.

 

국제화 파일 선택1

@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가 없으므로 message를 사용

 

국제화 파일 선택2

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

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

 

 

 

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

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

 

먼저 messages.properties에 메시지들을 추가로 등록해보자.

hello=안녕
hello.name =안녕 {0}

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.html

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

 

editForm.html

<!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.updateItem}">상품 수정 폼</h2>
    </div>

    <form action="item.html" th:action th:object="${item}" method="post">
        <div>
            <label for="id" th:text="#{label.item.id}">상품 ID</label>
            <input type="text" id="id" th:field="*{id}" class="form-control" readonly>
        </div>
        <div>
            <label for="itemName" th:text="#{label.item.itemName}">상품명</label>
            <input type="text" id="itemName" th:field="*{itemName}" class="form-control">
        </div>
        <div>
            <label for="price" th:text="#{label.item.price}">가격</label>
            <input type="text" id="price" th:field="*{price}" class="form-control">
        </div>
        <div>
            <label for="quantity" th:text="#{label.item.quantity}">수량</label>
            <input type="text" id="quantity" th:field="*{quantity}" class="form-control">
        </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='item.html'"
                        th:onclick="|location.href='@{/message/items/{itemId}(itemId=${item.id})}'|"
                        type="button" th:text="#{button.cancel}">취소</button>
            </div>
        </div>

    </form>

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

 

item.html

<!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.item}">상품 상세</h2>
    </div>

    <!-- 추가 -->
    <h2 th:if="${param.status}" th:text="'저장 완료'"></h2>

    <div>
        <label for="itemId" th:text="#{label.item.id}">상품 ID</label>
        <input type="text" id="itemId" name="itemId" class="form-control" value="1" th:value="${item.id}" readonly>
    </div>
    <div>
        <label for="itemName" th:text="#{label.item.itemName}">상품명</label>
        <input type="text" id="itemName" name="itemName" class="form-control" value="상품A" th:value="${item.itemName}" readonly>
    </div>
    <div>
        <label for="price" th:text="#{label.item.price}">가격</label>
        <input type="text" id="price" name="price" class="form-control" value="10000" th:value="${item.price}" readonly>
    </div>
    <div>
        <label for="quantity" th:text="#{label.item.quantity}">수량</label>
        <input type="text" id="quantity" name="quantity" class="form-control" value="10" th:value="${item.quantity}" readonly>
    </div>

    <hr class="my-4">

    <div class="row">
        <div class="col">
            <button class="w-100 btn btn-primary btn-lg"
                    onclick="location.href='editForm.html'"
                    th:onclick="|location.href='@{/message/items/{itemId}/edit(itemId=${item.id})}'|"
                    type="button" th:text="#{page.updateItem}">상품 수정</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="#{page.items}">목록으로</button>
        </div>
    </div>

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

 

items.html

<!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">
</head>
<body>
<div class="container" style="max-width: 600px">
    <div class="py-5 text-center">
        <h2 th:text="#{page.items}">상품 목록</h2>
    </div>
    <div class="row">
        <div class="col">
            <button class="btn btn-primary float-end"
                    onclick="location.href='addForm.html'" th:onclick="|location.href='@{/message/items/add}'|"
                    type="button" th:text="#{page.addItem}">상품 등록</button>
        </div>
    </div>
    <hr class="my-4">
    <div>
        <table class="table">
            <thead>
            <tr>
                <th th:text="#{label.item.id}">ID</th>
                <th th:text="#{label.item.itemName}">상품명</th>
                <th th:text="#{label.item.price}">가격</th>
                <th th:text="#{label.item.quantity}">수량</th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="item : ${items}">
                <td><a href="item.html" th:href="@{/message/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원id</a></td>
                <td><a href="item.html" th:href="@{|/message/items/${item.id}|}" th:text="${item.itemName}">상품명</a></td>
                <td th:text="${item.price}">10000</td>
                <td th:text="${item.quantity}">10</td>
            </tr>
            </tbody>
        </table>
    </div>
</div> <!-- /container -->
</body>
</html>

 

 

++ 참고로 파라미터는 다음과 같이 사용할 수 있다.

hello.name=안녕 {0}

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

 

정리

지금까지 메시지를 효율적으로 관리하는 방법을 알아보았다. 이제 여기에 더해서 국제화를 웹 애플리케이션에 어떻게 적용하는지 알아보자.

 

 

 

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

이번에는 웹 애플리케이션에 국제화를 적용해보자. 먼저 영어 메시지를 messages_en.properties에 추가해주자.

hello= hello
hello.name = hello {0}

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

 

사실 이것으로 국제화 작업은 거의 끝났다. 앞에서 템플릿 파일에는 모두 #{...}를 톻해 메시지를 사용하도록 적용해두었기 때문이다.

 

chrome에서 언어 설정 값을 변경하여 국제화 적용을 확인해볼 수 있다.

1. chrome 브라우저의 설정 버튼 클릭

2. 언어를 검색

3. '영어'를 우선순위로 변경

 

--> 웹 브라우저의 언어 설정 값을 변경하면 요청시 Accept-Language의 값이 변경된다.

 

Accept-Language의 우선 순위가 영어로 변경되었기 때문에 국제화 작업이 된 웹 브라우저를 볼 수 있을 것이다.

 

 

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

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

 

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

 

LocaleResolver

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

public interface LocaleResolver {
    Locale resolveLocale(HttpServletRequest var1);

    void setLocale(HttpServletRequest var1, @Nullable HttpServletResponse var2, @Nullable Locale var3);
}

 

만약 Locale 선택 방식을 변경하려면 LocaleResolver의 구현체를 변경해서 쿠키나 세션 기반의 Locale 선택 기능을 사용할 수 있다. 예를 들어 고객이 직접 Locale을 선택하도록 하는 것이다.

 

반응형

'공부 > Spring' 카테고리의 다른 글

검증2 - Bean Validation  (1) 2025.04.03
검증1 - Validation  (0) 2025.04.01
타임리프 - 스프링 통합과 폼  (0) 2025.03.29
타임리프 - 기본 기능  (0) 2025.03.27
스프링 MVC 웹 페이지 만들기  (0) 2025.03.25