김영한님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의 내용을 정리한 것입니다.
메시지, 국제화 소개
메시지
label에 있는 단어를 변경하려면 화면들을 다 찾아가서 하드코딩되어 있는 단어를 전부 변경해야한다. 상당히 비효율적 !
근데 메시지를 한 곳에서 관리하면 ? 찾아다니지 않아도 됨.
메시지 기능 : 다양한 메시지를 한 곳에서 관리하도록 하는 기능
예)
[messages.properties]
item=상품
item.id=상품 ID
item.itemName=상품명
item.price=가격
item.quantity=수량
각 HTML들은 해당 데이터를 key 값으로 불러 사용한다.
예)
[addForm.html]
<label for="itemName" th:text="#{item.itemName}"></label>
국제화
메시지에서 설정한 메시지 파일 (messages.properties)을 나라별로 별도로 관리하는 서비스를 국제화할 수 있다.
예)
[message_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", "errors");
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.properties]
spring.messages.basename=messages,config.i18n.messages
스프링 부트 메시지 소스 기본 값 : spring.messages.basename=messages
MessageSource를 스프링 빈으로 등록하지 않고, 스프링 부트와 관련된 별도의 설정을 하지 않으면 messages라는 이름으로 기본 등록된다. 따라서 messages_en.properties , messages_ko.properties , messages.properties 파일만 등록하면 자동으로 인식된다.
메시지 파일 만들기
- messages.properties :기본 값으로 사용(한글)
- messages_en.properties : 영어 국제화 사용
[/resources/messages.properties]
messages.properties
hello=안녕
hello.name=안녕 {0}
[/resources/messages_en.properties]
messages_en.properties
hello=hello
hello.name=hello {0}
스프링 메시지 소스 사용
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;
[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에서 설정한 기본 이름 메시지 파일을 조회한다. basename으로 messages를 지정 했으므로 messages.properties파일에서 데이터 조회한다.
[결과]

* 참고 (오류 해결, Mac)
https://myste-leee.tistory.com/173
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 추가 - 매개변수 사용
@Test
void argumentMessage() {
String result = ms.getMessage("hello.name", new Object[]{"Spring"}, null);
assertThat(result).isEqualTo("안녕 Spring");
}
다음 메시지의 {0} 부분은 매개변수를 전달해서 치환할 수 있다.
hello.name=안녕 {0} → Spring단어를매개변수로전달 → 안녕 Spring
[결과]

국제화 파일 선택
locale 정보를 기반으로 국제화 파일을 선택한다.
- Local이 en_US인 경우,
messages_en_US → messages_en → messages 순서로 찾는다. - Local 정보가 없는 경우,
- Local.getDefault()을 호출해서 시스템의 기본 local을 사용한다.
- locale = null → 시스템 기본 local이 ko_KR → messages_ko.properties 조회 시도 → 조회 실패 → messages.properties 조회 - Loacal에 맞추어 구체적인 것이 있으면 구체적인 것을 찾고 없으면 default를 찾는다.
MessageSourceTest 추가 - 국제화 파일 선택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가 없으므로 messages를 사용
MessageSourceTest 추가 - 국제화 파일 선택2
@Test
void enLang() {
assertThat(ms.getMessage("hello", null, Locale.ENGLISH)).isEqualTo("hello");
}
- ms.getMessage("hello", null, Locale.ENGLISH) : locale 정보가 Locale.ENGLISH이므 로 messages_en을 찾아서 사용
웹 애플리케이션 메시지 적용하기
[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=취소
타임리프의 메시지 표현식 #{...} 를 사용하면 스프링의 메시지를 편리하게 조회할 수 있다.
렌더링 전
<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>
페이지 이름에 적용
<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_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
[결과]
* 참고
Chrome → 설정 → 언어 → 영어를 가장 위로 하면 영어로 바뀜
'Tech > Spring | Spring Boot' 카테고리의 다른 글
[Spring][스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 5. 검증2 - Bean Validation (0) | 2024.01.31 |
---|---|
[Spring][스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 4. 검증1 - Validation (1) | 2024.01.30 |
[Spring][스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 2. 스프링 통합과 폼 (1) | 2024.01.29 |
[Spring][스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 1. 타임리프 - 기본 기능 (2) | 2024.01.25 |
[Spring][스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 7. 스프링 MVC - 웹 페이지 만들기 (1) | 2024.01.01 |
댓글