관리 메뉴

나구리의 개발공부기록

메시지 및 국제화, 프로젝트 설정 및 소개, 스프링 메시지 소스 설정, 스프링 메시지 소스 사용, 웹 애플리케이션에 메시지 적용하기, 웹 애플리케이션에 국제화 적용 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술

메시지 및 국제화, 프로젝트 설정 및 소개, 스프링 메시지 소스 설정, 스프링 메시지 소스 사용, 웹 애플리케이션에 메시지 적용하기, 웹 애플리케이션에 국제화 적용

소소한나구리 2024. 9. 3. 14:12

  출처 : 인프런 - 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술 (유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://inf.run/GMo43


1. 설정 및 소개

1) 프로젝트 설정

(1) 프로젝트

  • 제공된 소스코드의 message-start 폴더의 이름을 message로 변환하여 임포트해서 사용
  • Gradle
  • Java 11
  • Spring Boot 2.4.4

(2) Metadata

  • Group: hello
  • Artifact: message
  • Packaging: Jar

(3) Dependencies

  • 스프링 웹
  • 타임리프
  • 롬복

2) 소개

(1) 메시지

  • HTML 파일에 하드코딩된 메시지를 수정해야할 때 지금처럼 단순히 HTML파일이 몇개 없을 때에는 문제가 되지 않지만 HTML파일이 수십개라면 수십개의 파일을 모두 고쳐야함
  • HTML파일에 하드코딩 되어있는 단어들을 별도의 파일(ex: messages.properties)라는 메시지 관리용 파일을 만들어서 관리하는 기능을 메시지 기능이라함
  • 각 HTML들은 아래처럼 데이터를 key값으로 불러서 사용
<!-- 예시 -->
<label for="itemName" th:text="#{item.itemName}"></label>
<label for="itemName" th:text="#{item.itemName}"></label>

 

(2) 국제화

  • 사이트 국제화 -> 설정된 언어를 보고 HTML을 렌더링하는 것
  • messages.properties파일을 나라별로 별도로 관리 -> 언어별로 국제화 적용 가능
  • 접근 방법을 인식하는 방법은 HTTP accept-language 헤더값을 사용하거나 사용자가 직접 언어를 선택하도록 하고, 쿠키 등을 사용해서 처리할 수 있음
  • 스프링은 메시지와 국제화 기능을 통합해서 제공함

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

1) 직접 등록

(1) MessageSource

  • 스프링은 기본적인 메시지 관리 기능을 제공하며 이를 사용하려면 MessageSource 인터페이스를 스프링 빈으로 등록하면 됨
  • setBaseNames(): 설정 파일의 이름을 지정
  • 설정 파일의 이름을 messages로 지정하면 messages.properties파일을 읽어서 사용함
  • 추가로 국제화 기능을 적용하려면 messages_en.properties, messages_ko.properties와 같이 파일명 마지막에 언어 정보를 주면되며 찾을 수 있는 국제화 파일이 없으면 message.properties를 기본으로 사용함
  • 파일 위치는 /resources/messages.properties에 두면되며 여러 파일을 한번에 지정할 수 있음
  • defaultEncoding: 인코딩 정보를 지정, utf-8을 사용하면 됨
@Bean
public MessageSource messageSource() {
    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    messageSource.setBasenames("messages", "errors");
    messageSource.setDefaultEncoding("utf-8");
    return messageSource;
}

2) 스프링 부트

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

  • 스프링 부트를 사용하면 자동으로 MessageSource를 스프링 빈으로 등록함
  • 아래처럼 application.properties에 메시지 소스를 설정할 수 있음
  • 스프링 부트와 관련된 별도의 설정을 하지 않으면 messages라는 이름으로 기본 등록되며 국제화가 적용이되면messages_en.properties, messages_ko.properties, messages.properties 파일을 등록하면 자동으로 인식됨
spring.messages.basename=messages,config.i18n.messages

스프링 부트 메시지 소스 기본 값
spring.messages.basename=messages

3) 메시지 파일 만들기

(1) messages.properties

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

 

(2) messages_en.properties

hello=hello
hello.name=hello {0}

 

** 해당 과정 실습 시 messages.properties에 작성한 한글 파일 깨짐이 있었는데 인터넷 검색과 인프런 문의 게시판을 통해 임시 해결법을 찾은 결과 방법은 아래와 같음

  1. 인텔리제이 setting -> File Encodings -> Global Encoding : UTF - 8 , Default encoding for properties files : UTF - 8, Transparent, nateive-toascii conversion 체크
  2. Help -> Edit Custum VM Options -> -Duser.language=ko -Duser.country=KR 추가


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

1) 테스트 코드를 통한 학습

(1) MessageSource 인터페이스

  • MessageSource 인터페이스를 보면 코드를 폼한 일부 파라미터로 메시지를 읽어오는 기능을 제공함
  • ms.getMessage("hello", null, null)
  • code: hello, args: null, locale: null
package org.springframework.context;

public interface MessageSource {
    @Nullable
    String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
    String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
    String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}

 

(2) MessageSourceTest - 가장 단순한 테스트

  • test하위의 itemservice에 message패키지를 만든 후 작성
  • 메시지코드로 hello를 입력하고 나머지 값을 null을 입력
  • locale 정보가 없는 경우 Locale.getDefault()을 호출해서 시스템의 기본 로케일을 사용함
  • locale이 null인 경우 시스템 기본 locale이 ko_KR이므로 messages_ko.properties조회시도 -> 조회 실패 -> messages.properties 조회가 됨
// 메시지 코드로 hello , 나머지는 null
@Test
void helloMessage() {
    String result = ms.getMessage("hello", null, null);
    assertThat(result).isEqualTo("안녕");
// locale 정보가 없으면 basename에서 설정한 기본 이름 메시지 파일을 조회
// messages.properties파일에서 설정한 hello 데이터 조회 -> "안녕"이 조회됨
}

 

(3) MessageSourceTest 테스트 추가 - 메시지가 없는 경우, 기본 메시지

  • 메시지가 없는 경우에는 NoSuchMessageException이 발생함
  • getMessage()의 파라미터개수가 4개이면 3번째 파라미터에 메시지를 입력하여 기본 메시지를 사용할 수 있는데, 메시지가 없다면 메시지가 반환됨
// 메시지가 없는 경우에는 NoSuchMessageException이 발생함
@Test
void notFoundMessageCode() {
    assertThatThrownBy(() -> ms.getMessage("no_code", null, null))
            .isInstanceOf(NoSuchMessageException.class);
}

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

 

(4) MessageSourceTest 테스트 추가 - 매개변수 사용

  • message.properties에서 설정한 {0}부분을 매개변수를 전달하여 치환하여 사용할 수 있음
  • getMessage()의 2번재 파라미터에 입력
// {0} 부분을 매개변수를 전달해서 치환할 수 있음
@Test
void argumentMessage() {
    String result = ms.getMessage("hello.name", new Object[]{"Spring"}, null);
    assertThat(result).isEqualTo("안녕 Spring");
}

 

(5) MessageSourceTest 테스트 추가 - 국제화 파일 선택

  • 로케일 정보를 기반으로 국제화 파일을 선택함
  • Locale이 en_US의 경우 messages_en_US -> messages_un -> messages순서로 찾음
  • Locale에 맞추어 구체적인것을 찾고 없으면 디폴트를 찾음
@Test
void defaultLang() {
    // locale정보가 없음 -> messages 사용
    assertThat(ms.getMessage("hello", null, null)).isEqualTo("안녕");

    // locale정보가 있지만 messages_ko가 없음 -> messages 사용
    assertThat(ms.getMessage("hello", null, Locale.KOREA)).isEqualTo("안녕");
}

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

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

1) 메시지 적용

(1) 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 = 취소

 

(2) 타임리프 메시지 적용

  • 메시지 표현식인 #{...}을 사용
  • messages.properties에 등록한 '상품' 조회 -> #{label.item} 으로 입력하면 됨
  • 하드코딩 되어있는 각 html 파일에 적용됨
  • 텍스트에 수정이 있을때 html 파일의 문구를 하나하나 다 수정하는 것이 아니라, 한번에 일괄 수정할 수 있는 편리함이 있음

(3) addForm.html

<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" th:placeholder="#{placeholder.itemName}">
        </div>
        <div>
            <label for="price" th:text="#{label.item.price}">가격(하드코딩)</label>
            <input type="text" id="price" th:field="*{price}" class="form-control" th:placeholder="#{placeholder.price}">
        </div>
        <div>
            <label for="quantity" th:text="#{label.item.quantity}">수량(하드코딩)</label>
            <input type="text" id="quantity" th:field="*{quantity}" class="form-control" th:placeholder="#{placeholder.quantity}">
        </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 -->

 

(4) editForm.html

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

 

(5) item.html

<div class="container">

    <div class="py-5 text-center">
        <h2 th:text="#{page.item}">상품 상세(하드코딩)</h2>
    </div>

    <!-- 추가 -->
    <h2 th:if="${param.status}" th:text="#{label.item.complete}">저장 완료</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 -->

 

(6) items.html

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

 

(7) 파라미터 적용

  • hello.name = 안녕 {0}
  • {0}에 itme.itemName의 값이 치환되어 출력됨
  • th:text="#{hello.name('이렇게 직접적용')}" 해도 출력됨
<tr th:each="item : ${items}">
	<td th:text="#{hello.name(${item.itemName})}"></td>

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

1) 국제화 적용

(1) 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

 

(2) 웹으로 확인하기

  • 크롬브라우저 -> 설정 -> 언어를 검색하고 우선순위를 변경하면 모두 영어로 변환됨!
  • HTTP 요청 헤더의 Accept_Language의 값이 변경됨
  • 일부 예제 중 적용이 안된 곳은 직접 적용해주기!
<!-- messages_en.properties -->
label.item.complete=complete

placeholder.itemName = Please enter your name
placeholder.price = Please enter price
placeholder.quantity = Please enter quantity

<!-- messages.properties -->
label.item.complete = 저장 완료

placeholder.itemName = 이름을 입력하세요
placeholder.price = 가격을 입력하세요
placeholder.quantity = 수량을 입력하세요


<!-- addForm.html 및 item.html 수정 -->
<h2 th:if="${param.status}" th:text="#{label.item.complete}">저장 완료</h2>

<form action="item.html" th:action th:object="${item}" method="post">
    <div>
        <label th:text="#{label.item.itemName}">상품명(하드코딩)</label>
        <input type="text" id="itemName" th:field="*{itemName}" class="form-control" th:placeholder="#{placeholder.itemName}">
    </div>
    <div>
        <label th:text="#{label.item.price}">가격(하드코딩)</label>
        <input type="text" id="price" th:field="*{price}" class="form-control" th:placeholder="#{placeholder.price}">
    </div>
    <div>
        <label th:text="#{label.item.quantity}">수량(하드코딩)</label>
        <input type="text" id="quantity" th:field="*{quantity}" class="form-control" th:placeholder="#{placeholder.quantity}">
    </div>

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

(1) 스프링의 국제화 메시지 선택 방법

  • 메시지 기능은 Locale 정보를 알아야 언어를 선택할 수 있으므로 스프링도 Locale 정보를 알아야 언어를 선택할 수 있음
  • 기본적으로 스프링은 Accept-Language 헤더의 값을 사용함
  • 스프링은 Locale 선택 방식을 변경할 수 있도록 LocaleResolver라는 인터페이스를 제공하는데, 스프링 부트는 기본으로 Accept-Language를 활용하는 AcceptHeaderLocaleResolver를 사용함

(2) LocaleResolver 인터페이스

  • 만약 Locale 선택 방식을 변경하려면 LocaleResolver의 구현체를 변경해서 쿠키나 세션 기반의 Locale 선택 기능을 사용할 수 있음
  • 즉, 브라우저 설정을 변경하는 것이 아니라 팝업 혹은 버튼 등을 활용해서 고객이 직접 Locale을 선택하도록 하는 방식처럼 적용할 수 있음
  • 메시지는 많이 활용하지만 국제화는 global 홈페이지를 만들 때만 사용하므로 자세한 내용은 실제 적용시 검색하여 적용
public interface LocaleResolver {
     Locale resolveLocale(HttpServletRequest request);
     void setLocale(HttpServletRequest request,
                    @Nullable HttpServletResponse response,
                    @Nullable Locale locale);
}