Super Kawaii Cute Cat Kaoani [스프링 MVC 2편] 3. 메시지, 국제화

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

2024. 1. 25. 14:56
728x90
SMALL

📌 메시지, 국제화 소개

⚡ 메시지 기능

  • 상품명이라는 단어를 모두 상품이름으로 고쳐야할 때 사용

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

item=상품
item.id=상품 ID
item.itemName=상품명
item.price=가격
item.quantity=수량
  • 각 HTML들은 다음과 같이 해당 데이터를 key 값으로 불러서 사용

 

addForm.html

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

 

 

editForm.html

<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

 

🔶 messages_ko.properties

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

 

✔ 어느 지역에서 접근한 것인지 인식하는 방법

  • HTTP accept-language 해더 값을 사용하거나 사용자가 직접 언어를 선택하도록 하고, 쿠키 등을 사용해서 처리

▪ 스프링은 기본적인 메시지와 국제화 기능을 모두 제공
▪ 타임리프도 스프링이 제공하는 메시지와 국제화 기능을 편리하게 통합해서 제공

 

📌 스프링 메시지 소스 설정

✔ 메시지 관리 기능

  • 스프링이 제공하는 MessageSource 를 스프링 빈으로 등록해서 사용
    • MessageSource : 인터페이스
  • 구현체인 ResourceBundleMessageSource 를 스프링 빈으로 등록

 

⚡ 직접 등록

    @Bean
    public MessageSource messageSource(){
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasenames("messages", "error");
        messageSource.setDefaultEncoding("utf-8");
        return messageSource;
    }
  • basenames : 설정 파일의 이름을 지정
    • messages 로 지정하면 messages.properties 파일을 읽어서 사용
      ➡ 국제화 기능을 적용하려면 messages_en.properties , messages_ko.properties 와 같이 파일명 마지막에 언어 정보 제공
      ➡ 만약 찾을 수 있는 국제화 파일이 없으면 messages.properties (언어정보가 없는 파일명)를 기본으로 사용
      ➡ 파일의 위치는 /resources/messages.properties
    • 여러 파일을 한번에 지정 가능
      • 여기서는 messages , errors 둘을 지정
  • defaultEncoding : 인코딩 정보를 지정한다. utf-8 을 사용

 

 

⚡ 스프링 부트

  • 스프링 부트가 MessageSource 를 자동으로 스프링 빈으로 등록

✔ 메시지 소스 설정

🔶 application.yml

spring:
  messages:
    basename: messages, cofing.i18n.messages

 

✔ 메시지 소스 기본 값

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

 

메세지 파일 만들기

  • messages.properties :기본 값으로 사용(한글)
  • messages_en.properties : 영어 국제화 사용

 

🔶 /resources/messages.properties

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

 

 

🔶 /resources/messages_en.properties

hello=hello
hello.name=hello {0}

 

 

📌 스프링 메시지 소스 사용

⚡ MessageSource 인터페이스

package hello.itemservice.message;

import jakarta.annotation.Nullable;
import org.springframework.context.NoSuchMessageException;

import java.util.Locale;

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;
}
  • 코드를 포함한 일부 파라미터로 메시지를 읽어오는 기능을 제공

 

⚡ Test

MessageSourceTest

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 static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class MessageSourceTest 
{

    @Autowired
    MessageSource ms;

    @Test
    void helloMessage(){
        String result = ms.getMessage("hello", null, null);
        assertThat(result).isEqualTo("안녕");
    }
}
  • ms.getMessage("hello", null, null)
    • code: hello
    • args: null
    • locale: 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("기본 메시지");
    }
  • 메시지가 없는 경우에는 NoSuchMessageException 이 발생
  • 메시지가 없어도 기본 메시지( defaultMessage )를 사용하면 기본 메시지가 반환됨

 

MessageSourceTest 추가 - 매개변수 사용

    /**
     * 매개변수 사용
     */
    void argumentMessage(){
        String result = ms.getMessage("hello.name", new Object[]{"Spring"}, null);
        assertThat(result).isEqualTo("안녕 Spring");
    }
  • 다음 메시지의 {0} 부분은 매개변수를 전달해서 치환할 수 있음
  • hello.name=안녕 {0} -> Spring 단어를 매개변수로 전달 -> 안녕 Spring

 

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

✔ 국제화 파일 선택

  • locale 정보를 기반으로 국제화 파일을 선택
  • Locale이 en_US 의경우 messages_en_US messages_en messages 순서로 찾음
  • Locale 에 맞추어 구체적인 것이 있으면 구체적인 것을 찾고, 없으면 디폴트를 찾음
    /**
     * 국제화 파일 선택
     */
    @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 를 사용

 

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

    @Test
    void enLang(){
        assertThat(ms.getMessage("hello", null, Locale.ENGLISH)).isEqualTo("hello");
    }
  • ms.getMessage("hello", null, Locale.ENGLISH) : locale 정보가 Locale.ENGLISH 이므로 messages_en 을 찾아서 사용

Locale 정보가 없는 경우 Locale.getDefault() 을 호출해서 시스템의 기본 locale을 사용

ex) locale = null 인 경우 시스템 기본 locale 이 ko_KR 이므로 messages_ko.properties 조회 시도
➡ 조회 실패
➡ messages.properties 조회
참고 링크
참고 링크

 

 

반응형

 

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

🔶 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=취소가

 

⚡ 타임리프 메시지 적용

  • 타임리프의 메시지 표현식 #{...} 를 사용하면 스프링의 메시지를 편리하게 조회 가능
  • ex) 방금 등록한 상품이라는 이름을 조회하려면 #{label.item}

✔ 렌더링 전

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

 

✔ 렌더링 후

<div>상품</h2>

 

 

⚡ 적용 대상

addForm.html
editForm.html
item.html
items.html

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>

    <h4 class="mb-3">상품 입력</h4>

    <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>상품 등록 폼</h2>
<h2 th:text="#{page.addItem}">상품 등록</h2>

✔ 레이블에 적용

  • <label for="itemName">상품명</label>
<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">상품 등록</button>
<button type="submit" th:text="#{button.save}">저장</button>
<button type="button" th:text="#{button.cancel}">취소</button>

 

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>

 

실행

  • 잘 동작하는지 확인하기 위해 messages.properties 파일의 내용을 가격 금액과 같이 변경해서 확인
  • 정상 동작하면 다시 돌려두자.

 

✔ 파라미터 사용법

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

 

 

 

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

🔶 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

✔ 웹으로 확인

  • 크롬 브라우저 설정 언어를 검색하고, 우선 순위를 변경

Accept-Language

  • 클라이언트가 서버에 기대하는 언어 정보를 담아서 요청하는 HTTP 요청 헤더

 

 

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

  • Locale 정보를 알아야 언어를 선택 가능
  • 스프링도 Locale 정보를 알아야 언어를 선택 가능
  • 스프링은 언어 선택시 기본으로 Accept-Language 헤더의 값을 사용한다.

 

 

⚡ LocaleResolver

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

 

 

✔ LocaleResolver 인터페이스

public interface LocaleResolver {
    Locale resolveLocale(HttpServletRequest request);
    void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);
}

 

✔ LocaleResolver 변경

  • LocaleResolver 의 구현체를 변경해서 쿠키나 세션 기반의 Locale 선택 기능 사용 가능
  • 고객이 직접 Locale 을 선택하도록 하는 것

 

 

김영한님의 <스프링 MVC 2편 - 백엔드 웹 개발 활용 기술>을 수강하고 작성한 글입니다

 

 

 


PREV

 

[스프링 MVC 2편] 2. 타임리프 - 스프링 통합과 폼

📌 프로젝트 설정 스프링 MVC 1편에서 마지막에 완성했던 상품 관리 프로젝트를 약간 변형해서 사용 ✔ 개발 환경 Java 17 IntelliJ IDEA ✔ 실행 http://localhost:8080 http://localhost:8080/form/items 📌 타임리프

nyeroni.tistory.com

 

NEXT

 

[스프링 MVC 2편] 4. 검증1 - Validation

📌 검증 요구사항 ⚡ 요구사항 : 검증 로직 추가 타입 검증 가격, 수량에 문자가 들어가면 검증 오류 처리 필드 검증 상품명: 필수, 공백X 가격: 1000원 이상, 1백만원 이하 수량: 최대 9999 특정 필

nyeroni.tistory.com

 

728x90
LIST

BELATED ARTICLES

more