Last Modified 2017.10.27

국제화

예제 소스 : https://github.com/kimjonghoon/i18nOnSpringMVC
루트 티렉터리에서 mvn jetty:run을 실행한 후, http://localhost:8080에 방문한다.

메시지 소스

메시지 소스(MessageSource)는 로케일을 보고 메시지를 결정하는 컴포넌트다.
구현체로는 ResourceBundleMessageSource와 ReloadableResourceBundleMessageSource가 있다.

ResourceBundleMessageSource는 클래스 패스에 있는 리소스만 접근할 수 있다.
ReloadableResourceBundleMessageSource는 리소스가 파일 시스템에 있으면 어디든 접근할 수 있다.

SpringBbs 프로젝트의 스프링 설정 파일에 다음을 추가한다.

<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
	<property name="basename" value="WEB-INF/classes/messages" />
	<property name="defaultEncoding" value="UTF-8" />
</bean>

<property name="defaultEncoding" value="UTF-8" />을 없으면 한글이 깨진다.

로케일 리졸버 설정 추가

로케일 리졸버(LocaleResolver)는 로케일을 결정하는 컴포넌트다.
LocaleResolver 설정을 생략하면 디폴트로 AcceptHeaderLocaleResolver가 선택된다.
AcceptHeaderLocaleResolver는 요청 헤더의 "accept-language"에 설정된 로케일을 사용한다.
accept-language 헤더에는 운영체제의 로케일이 세팅된다.
이경우 사용자가 로케일을 변경할 수 없다.
사용자가 로케일을 변경할 수 있게 하려면 LocaleResolver로 SessionLocaleResolver 나 CookieLocaleResolver를 선택해야 하고 LocaleChangeInterceptor가 필요하다.

SpringBbs 프로젝트의 스프링 설정 파일에 SessionLocaleResolver 설정과 LocalChangeInterceptor 설정을 추가한다.

spring-bbs-servlet.xml
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
    <property name="defaultLocale" value="en" />
</bean>
<mvc:interceptors>
	<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
		<property name="paramName" value="lang" />
	</bean>
</mvc:interceptors>

JSP 수정

국제화를 적용할 JSP에 스프링 태그 라이브러리 지시어를 추가한다.

<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>

list.jsp의 검색 버튼의 value 값에 국제화를 적용해 보자.
list.jsp에 스프링 태그 라이브러리 지시어를 추가한 후, 검색 폼에서 서밋 버튼의 value 속성값을 다음과 같이 수정한다.

<input type="submit" value="<spring:message code="global.search" />" />

src/java/resources에 내용이 없는 messages_en.properties와 messages_ko.properties 파일을 생성한다.
아래 그림처럼 프로퍼티 파일 인코딩을 UTF-8로 변경한다.
메시지 리소스 파일를 선택하고 컨텍스트 메뉴를 오픈하고 Properties 선택한다.
메시지 리소스 파일의 Text file encoding을 UTF-8로 변경한다.

프로퍼티 파일을 다음과 같이 작성한다.

messages_en.properties
global.search = Search
messages_ko.properties
global.search = 검색

header.jsp에 로케일 변경 링크를 추가한다.

header.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="security" %>
<h1 style="float: left;width: 150px;"><a href="/"><img src="/images/ci.gif" alt="java-school" /></a></h1>
<div id="memberMenu" style="float: right;position: relative;top: 7px;">
<security:authorize access="hasAnyRole('ROLE_USER','ROLE_ADMIN')">
	<security:authentication property="principal.username" var="check" />
</security:authorize>
<c:choose>
	<c:when test="${empty check}">
		<input type="button" value="<spring:message code="user.login" />" onclick="location.href='/users/login'" />
		<input type="button" value="<spring:message code="user.signup" />" onclick="location.href='/users/signUp'" />
	</c:when>
	<c:otherwise>
		<input type="button" value="<spring:message code="user.logout" />" id="logout" />
		<input type="button" value="<spring:message code="user.modify.account" />" onclick="location.href='/users/editAccount'" />
	</c:otherwise>
</c:choose>
</div>

<%
String url = "";
String english = "";
String korean = "";
String qs = request.getQueryString();
if (qs != null) {
	if (qs.indexOf("&lang=") != -1) {
		qs = qs.substring(0, qs.indexOf("&lang="));
	}
	if (qs.indexOf("lang=") != -1) {
		qs = qs.substring(0, qs.indexOf("lang="));
	}
	if (!qs.equals("")) {
	    String decodedQueryString = java.net.URLDecoder.decode(request.getQueryString(), "UTF-8");
	    url = "?" + decodedQueryString;
	    if (url.indexOf("&lang=") != -1) {
	        url = url.substring(0, url.indexOf("&lang="));
	    } 
	    english = url + "&lang=en";
	    korean = url + "&lang=ko";
	} else {
	    english = url + "?lang=en";
	    korean = url = "?lang=ko";
	}
} else {
    english = url + "?lang=en";
    korean = url = "?lang=ko";
}

pageContext.setAttribute("english", english);
pageContext.setAttribute("korean", korean);
%>
<div id="localeChangeMenu" style="float: right;position: relative;top: 7px;margin-right: 10px;">
    <input type="button" value="English" onclick="location.href='${english }'" />
    <input type="button" value="Korean" onclick="location.href='${korean }'" />
</div>

<form id="logoutForm" action="/logout" method="post" style="display:none">
	<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
</form>
<script type="text/javascript" src="/js/jquery-3.0.0.min.js"></script>

<script>
$(document).ready(function() {
	$('#logout').click(function() {
		$('#logoutForm').submit();
		return false;
  	});
});
</script>

데이터베이스를 이용하는 국제화 (게시판 이름)

게시판 코드(chat, qna, data)를 이용해서 리소스에 다음과 같이 추가할 수 있다.

bbs.board.chat = 자유 게시판
bbs.board.qna = 묻고 답하기
bbs.board.data = 자료실
bbs.board.chat = Chat
bbs.board.qna = QnA
bbs.board.data = Data

하지만 이경우 게시판을 추가할 때 리소스 메시지를 추가해주어야 한다.
프로그램적으로 리소스 메시지를 추가할 수 없고, 또 그래서도 안 된다.
리소스가 적용되려면 매번 컴파일과 서버 재시작이 필요하다.

로케일마다 게시판 이름 컬럼을 두는 방법도 있다.
현재 board 테이블의 구조는 게시판 이름을 한글로만 저장하고 있다.

$ sqlplus java/school
SQL> desc board
Name	    Null?    Type
 ---------------------------------------------
 BOARDCD	   NOT NULL VARCHAR2(20)
 BOARDNM	   NOT NULL VARCHAR2(40)
 
SQL> select * from board;
BOARDCD   BOARDNM
--------  -----------
chat      자유게시판
qna       묻고 답하기
data      자료실

기존 boardnm 컬럼은 영어 게시판 이름을, 새로 추가할 boardnm_ko는 한글 게시판 이름을 저장하기로 하자.

SQL> alter table board add (boardnm_ko varchar2(40));

SQL> update board set boardnm_ko = boardnm;

SQL> update board set boardnm = 'Chat' where boardcd = 'chat';
SQL> update board set boardnm = 'QnA' where boardcd = 'qna';
SQL> update board set boardnm = 'Data' where boardcd = 'data';

SQL> select * from board;
BOARDCD   BOARDNM   BOARDNM_KO
-----------------------------------
chat      Chat      자유 게시판
qna       QnA       묻고 답하기
data      Data      자료실
BoardMapper.xml
<select id="selectOneBoard" parameterType="string" resultType="Board">
	SELECT * FROM board WHERE boardcd = #{boardCd}
</select>
<select id="selectAllBoards" resultType="Board">
	SELECT * FROM board
</select>
BoardMapper.java
//게시판
public Board selectOneBoard(String boardCd);
//게시판 목록
public List<Board> selectAllBoards();
BoardService.java
//게시판
@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER')")
public Board getBoard(String boardCd);
//게시판 목록
@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER')")
public List<Board> getAllBoards();
BoardServiceImpl.java
//게시판
@Override
public Board getBoard(String boardCd) {
	return boardMapper.selectOneBoard(boardCd);
}
//게시판 목록
@Override
public List<Board> getAllBoards() {
	return boardMapper.selectAllBoards();
}
bbs-sub.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>

<!-- 중간 생략 -->

<c:forEach var="board" items="${boards }" varStatus="status">
	<li><a href="/bbs/list?boardCd=${board.boardCd }&page=1">${board.boardNm }</a></li>
</c:forEach>

<!-- 중간 생략 -->
bbs-sub_ko.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>

<!-- 중간 생략 -->

<c:forEach var="board" items="${boards }" varStatus="status">
	<li><a href="/bbs/list?boardCd=${board.boardCd }&page=1">${board.boardNm_ko }</a></li>
</c:forEach>

<!-- 중간 생략 -->
BbsController.java
private String getBoardName(String boardCd, String lang) {
	Board board = boardService.getBoard(boardCd);
	
	switch (lang) {
	case "en":
		return board.getBoardNm();
	case "ko":
		return board.getBoardNm_ko();
	default:
		return board.getBoardNm();
	}
}

BbsController에서 게시판 이름을 뷰에 전달해야 하는 메소드는 java.util.Locale locale 파라미터를 추가한 후 다음을 참조하여 수정한다.

String lang = locale.getLanguage();
String boardName = this.getBoardName(boardCd, lang);

빈 검증 국제화

다음은 현재 웹 사이트의 상태다. 로케일을 변경하더라도 빈 검증 에러 메시지는 로케일에 따라 변경되지 않는다.

스프링 설정 파일에 다음 설정을 추가한다.

<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
	<property name="validationMessageSource" ref="messageSource" />
</bean>

기존 설정에 아래 강조된 설정을 추가한다.

<mvc:annotation-driven validator="validator" />

messages_en.properties에 다음을 추가한다.

fullname.validation.error = The full name must exceed two characters.
mobile.validation.error = It is not a mobile phone number.
passwd.validation.error = Your password must be at least 4 characters long.

messages_ko.properties에 다음을 추가한다.

fullname.validation.error = 이름은 2자 이상이어야 합니다.
mobile.validation.error = 모바일 폰 형식이 아닙니다.
passwd.validation.error = 패스워드는 4자 이상이어야 합니다.

User.java에서 강조된 부분을 수정한다.

@Size(min=4, message="{passwd.validation.error}")
private String passwd;
@Size(min=2, message="{fullname.validation.error}")
private String name;
@Size(min=6, message="{mobile.validation.error}")
private String mobile;

서버를 재시작한 후 내 정보 수정을 다시 방문한다.
로케일을 영어권으로 선택한 후 입력란을 모두 삭제한 다음 전송 버튼을 누른다.

내용인 긴 본문 국제화

내용이 긴 본문을 국제화하는 데는 정답이 없다.
다만 프로퍼티 파일에 본문을 욱여넣지는 않는다.
필자가 선택한 방법은 아파치 타일즈를 이용하는 것이다.
아파치 타일즈의 정의 파일을 로케일마다 따로 만들면 된다.
영어권 로게일의 타일즈 정의 파일 이름이 template.xml이라면 한글용은 template_ko.xml을 만들면 국제화가 적용된다.