[Spring] 메시지, 국제화

bien·2023년 8월 1일
0

Spring_MVC2

목록 보기
1/7

메시지, 국제화 소개

메시지

코드에 직접 적힌 코드를 일일이 수정하는 것은 보통일이 아니다.
만약 기획자가 html의 라벨 속 상품명 이란 단어를 모두 상품이름으로 변경해 달라고 하면 어떻게 해야할까? 일일이 고치면 될 것 같아 보이지만, 많은 코드들을 실수없이 변경하는 것은 쉬운일이 아니다.

이처럼 html에 하드코딩 되어 있는 단어들을 변경하는 것은 쉽지 않다. 대신, 이같은 다양한 문자들을 한 곳에서 관리하는 기능을 메시지 기능이라 한다.

예를들어 message.properties라는 메시지 관리용 파일을 만들고

item = 상품
item.id = 상품 ID
item.itemName = 상품명
item.price = 가격
item.quantity = 수량
(key = value)

이제 HTML에서는 각 데이터들을 key 값으로 불러서 사용하면된다.

addForm.html

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

eddForm.html

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

이처럼 서로 다른 파일이더라도 하드코딩을 하지 않고 메시지 기능을 사용하면, 메시지 관리용 파일의 값을 바꾸는 것 만으로 한번에 모든 글자들을 변경할 수 있다.

작은 애플리케이션에서는 크게 중요하지 않으나, 프로젝트의 크기가 커지면 이같은 일관성있는 관리가 매우 중요해진다.

국제화

한국에서 들어가면 한국어로 보이고, 미국에서 들어가면 영어로 보이고, 독일에서 접속하면 독일어로 보이는 기능. 이 같은 국제화 기능은 메시지에서 조금만 더 나가면 된다. 이는 메시지 파일을 나라별로 관리하고, 영어이면 영어로 작성된 파일을, 한국이면 한국어로 작성된 파일을 제공하는 것이다. 이런 식으로 사이트를 국제화할 수 있다.

message_ko.properties

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

message_en.properties

item = item
item.id = item.id
item.itemName = item.itemName
item.price = item.price
item.quantity = item.quantity

언어 인식 방법 1. HTTP의 accept-language헤더 값 사용
언어 인식 방법 2. 사용자가 직접 언어 선택

스프링은 기본 국제화 기능을 제공하고 있다. 타임리프도 메시지, 국제화 기능을 편리하게 통합해서 제공한다.


스프링 메시지 소스 설정

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

직접 등록

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

스프링 부트

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

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

application.properties

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

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

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

메시지 파일 만들기

메시지 파일을 만들어보자!

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

주의! 파일명은 mesage가 아니라 messages다! 마지막에 s에 주의하자

messages.properties

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

messages_en.properties

hello=hello
hello.name=hello {0}

이렇게 파일명을 설정하면 intellij가 보기좋게 Resrce Bundle 'messages'로 묶어서 보여준다.


스프링 메시지 소스 사용

Message Source 인터페이스

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;

MessageSource가 스프링 빈에 등록되고, 우리는 이것을 가져다 테스트에서 사용해볼 것이다.

MessageSourceTest

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

package hello.itemservice.message;

 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 static org.assertj.core.api.Assertions.*;
 
 @SpringBootTest
 
 public 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 (국제화에 사용되는 변수)

메시지 코드로 hello를 입력하고 나머지 값은 null을 입력했다. locale정보가 없으면 basename에서 설정한 기본 이름(message) 메시지 파일을 조회한다. 따라서 message.properties파일에서 데이터를 조회한다.

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

@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("기본 메시지");
 }

ms.getMessage("no_code", null, "기본 메시지", null);

  • 메시지가 없는 경우에는 NoSuchMessageExeption이 발생한다.
  • 메시지가 없어도 기본 메시지(defaultMessage)를 사용하면 기본 메시지가 반환된다.

매개변수 사용

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

ms.getMessage("hello.name", new Object[]{"Spring"}, null);

  • new Object[]{"spring"}으로 배열을 넘긴다.

국제화 파일 선택

locale 정보를 기반으로 국제화 파일을 선택한다.

  • Locale이 en_US의 경우 message_en_US -> message_en -> message 순서로 찾는다.
  • Locale에 맞추어 구체적인 것이 있으면 구체적인것을 찾고, 없으면 디폴트를 찾는다고 이해하면 된다.

국제화 파일 선택 1

@Test
 void defaultLang() {
	 assertThat(ms.getMessage("hello", null, null)).isEqualTo("안녕");
     	// locale 정보가 없으므로 messages 를 사용
 	 assertThat(ms.getMessage("hello", null, Locale.KOREA)).isEqualTo("안녕");
     	//  locale 정보가 있지만, message_ko 가 없으므로 messages 를 사용

국제화 파일 선택 2

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

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

messages.properteis

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>

적용

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

파라미터 사용

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의 값이 변경된다.

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

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

앞서 MessageSource 테스트에서 보았듯이 메시지 기능은 Locale정보를 알아야 언어를 선택할 수 있다. 결국 스프링도 Locale정보를 알아야 언어를 선택할 수 있는데, 스프링은 언어 선택시 기본으로 Accept-Language헤더의 값을 사용한다.

그러나 이같은 선택 방식도 바꿀 수 있다. 사용자가 웹 페이지에 들어왔을 때, (웹 브라우저의) 설정과 무관하게 언어를 바꿀 수 있도록 하려면 다른 방식이 필요하다. 이때, LocaleResolver를 사용할 수 있다.

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

만약 Locale 선택 방식을 변경하려면 LocaleResolver의 구현체를 변경해서 쿠키나 세션 기반의 Locale선택 기능을 사용할 수 있다. 웹 브라우저에서 사용자가 언어를 변경하면, 이를 쿠키나 세션에 저장하여 계속 해당 언어로 사이트를 제공할 수 있다. 관련된 예제는 검색해서 사용해보자.



Reference

  • inflearn 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 김영한
profile
Good Luck!

0개의 댓글