Spring

[인프런/스프링 MVC 2편] 3. 메시지, 국제화

주니어주니 2023. 5. 18. 22:41

 

1. 메시지와 국제화

 

메시지

다양한 메시지를 한 곳에서 관리 

 

"상품명" 이라는 단어를 "상품이름"으로 수정하고자 할 때

모든 파일들을 다 찾아가면서 "상품명"이라는 단어를 변경해야 함 

-> 해당 HTML 파일에 메시지가 하드코딩 되어있기 때문 (메시지가 박혀있음)

-> 메시지를 한 곳에서 관리

 

ex) messages.properties 메시지 관리용 파일

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

 

각 HTML들은 해당 데이터를 key 값으로 불러서 사용

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

 

 

국제화

메시지 파일(messages.properties)을 각 나라별로 별도로 관리

 

ex) messages_en.properties

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

 

ex) messages_ko.properties

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

 

 

언어 인식 방법 : HTTP accept-language 헤더 값 or 사용자가 직접 언어 선택 or 쿠키 사용해서 처리 등

 

스프링 -> 메시지, 국제화 기능 제공 

 

 

 


 

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

 

메시지 관리 기능을 사용하려면 스프링이 제공하는 MessageSource를 스프링 빈으로 등록해야 함

MessageSource는 인터페이스 -> 구현체인 ResourceBundleMessageSource를 스프링 빈으로 등록

 

 

1) 직접 등록 (굳이 직접 하려면)

 

*ItemServiceApplication.java

@Bean
public MessageSource messageSource() {
    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    messageSource.setBasenames("messages", "errors");
    messageSource.setDefaultEncoding("utf-8");
    return messageSource;
}
  • setBasenames() : 설정 파일의 이름 지정
    • "messages" : messgaes.properties
      "errors" : errors.properties
    • 파일의 위치는 /resources/messages.properties
    • 여러 파일을 한번에 지정할 수 있음. (messages, errors)
  • setDefaultEncoding() : 인코딩 정보 지정 

 

 

2) 스프링 부트 사용

MessageSource를 자동으로 스프링 빈으로 등록해줌

 

 

(1) 스프링 부트 메시지 소스 설정

 

*application.properties 

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

resources 아래 -> config 아래 -> i18n 아래 -> messages.properties 라는 파일로 저장

 

 

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

spring.messages.basename=messages

 

별도의 설정을 하지 않으면 messages 라는 이름으로 기본 등록

(messages_en.properties, messages.properties 파일만 등록하면 자동으로 인식)

 

 

(2) 메시지 파일 만들기 

 

* /resources/messages.properties

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

 

* /resources/messages_en.properties

hello=hello
hello.name=hello {0}

 

 


 

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

 

* MessageSource 인터페이스 

  • 코드를 포함한 일부 파라미터로 메시지를 읽어오는 기능 제공
public interface MessageSource {
	String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
	String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;

 

 

 

1) 메시지 정보 O, 매개변수 X, 국제화 정보 X  인 경우 테스트 

 

* test/java/hello/itemservice/message.MessageSourceTest.java

@SpringBootTest
public class MessageSourceTest {

    @Autowired
    MessageSource ms;

    @Test
    void helloMessage() {
        String result = ms.getMessage("hello", null, null);
        assertThat(result).isEqualTo("안녕");
    }
  • 메시지 코드로 "hello"를 줬을 때,
    locale 정보가 없으면 시스템의 기본 로케일 사용 (ko인 경우 -> messages_ko.properties 조회 -> 없으면 messages.properties 조회) 
    • code : hello
    • args : null
    • locale : null

 

 

 

2) 메시지 X (no_code) 테스트 

@Test
void notFoundMessageCode() {
    assertThatThrownBy(() -> ms.getMessage("no_code", null, null))
            .isInstanceOf(NoSuchMessageException.class);
}
  • 메시지가 없는 경우 -> NoSuchMessgaeException 발생

 

 

 

3) 메시지 X, 기본메시지 사용 

@Test
void notFoundMessageCodeDefaultMessage() {
    String result = ms.getMessage("no_code", null, "기본 메시지", null);
    assertThat(result).isEqualTo("기본 메시지");
}
  • 메시지가 없어도 기본메시지(defaultMessage)를 사용하면 기본메시지 반환

 

 

 

4) 매개변수 사용

@Test
void argumentMessage() {
    String message = ms.getMessage("hello.name", new Object[]{"Spring"}, null);
    assertThat(message).isEqualTo("안녕 Spring");
}
  • messages.properties 파일의 hello.name=안녕 {0} 
    -> {0} 부분은 매개변수를 전달해서 치환할 수 있음
    -> new Object[]를 통해 매개변수 전달
    -> 안녕 Spring

 

 

 

5) 국제화

  • 시스템의 locale 정보를 기반으로 국제화 파일 선택
    • 구체적인 것이 있으면 구체적인 것을 찾고, 없으면 디폴트 (messages)  
@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");
}
  • ms.getMessage("hello", null, null) : locale 정보가 없으므로 (시스템 locale이 message_ko인데, 없으니까) messages 사용
  • ms.getMessage("hello", null, Locale.KOREA) : locale 정보가 있지만, message_ko가 없으므로 messages 사용
  • ms.getMessage("hello", null, Locale.ENGLISH) : locale 정보가 Locale.ENGLISH이므로 messages_en 사용

 

 


4. 웹 애플리케이션에 메시지 적용

 

1) 메시지 등록

 

* messages.properties 

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

label.item=상품
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=저장2
button.cancel=취소2

 

 

2) 메시지 적용 

 

* addForm.html, editForm.html, item.html, items.html 

 

(1) 페이지 이름에 적용 

<h2 th:text="#{page.addItem}">상품 등록</h2>
<h2 th:text="#{page.updateItem}">상품 수정</h2>
<h2 th:text="#{page.items}">상품 목록</h2>

 

(2) 레이블에 적용

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

 

(3) 버튼에 적용

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

 

 

* addItem.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>

 

 


 

5. 웹 애플리케이션에 국제화 적용

 

1) 영어 메시지 추가 

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

 

 

2) 구글 설정 -> 언어 -> 영어를 제일 위로 이동 

 

언어 설정 값 변경 -> 요청시 Accept-Language 값 변경