public interface LocaleResolver {
/**
* Resolve the current locale via the given request.
* Can return a default locale as fallback in any case.
* @param request the request to resolve the locale for
* @return the current locale (never {@code null})
*/
Locale resolveLocale(HttpServletRequest request);
/**
* Set the current locale to the given one.
* @param request the request to be used for locale modification
* @param response the response to be used for locale modification
* @param locale the new locale, or {@code null} to clear the locale
* @throws UnsupportedOperationException if the LocaleResolver
* implementation does not support dynamic changing of the locale
*/
void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);
}
기본 메시지 기능 V2
- 기능
- 사용자 선택에 따른 국제화 기능 구현
- 목적
- 다양한 LocaleResolver을 통해 국제화 기능 구현
프로젝트 환경 설정
- 의존관계 설정
- Thymeleaf
- Lombok
- Spring Web
- Gradle
- Java 11
- Jar
국제화 기능 구현
국제화 기능을 구현하기 위해서는 어떤 언어로 페이지를 보여줄지 정해야한다. 이 때 어떤 언어인지 정해야하는데 그럴 때 필요한 것이 LocaleResolver 이다.
LocaleResolver 인터페이스는 클라이언트의 언어 & 국가 정보를 인식하는 인터페이스다. 스프링 MVC는 LocaleResolver를 이용해서 웹 요청과 관련된 Locale을 추출하고, 이 Locale 객체를 이용해서 알맞은 언어의 메시지를 선택하게 된다. 지금부터 LocaleResolver를 사용해서 Locale을 변경하는 방법들에 대해서 살펴보도록 하겠다.
참고로 메시지 기능을 구현할 때 메시지 파일을 먼저 만들어야 한다. 이 때 메시지 파일명이 중요한데 기본 설정 메시지 파일명은 messages 다. 해당 메시지 파일들은 LocaleResolver를 구현할 때 공통으로 사용할 메시지 파일들이다.
📌 application.properties
spring.messages.basename=messages // 생략 가능
원래는 이렇게 messages 라는 파일 명을 등록해줘야하나 스프링 부트에서는 기본 설정으로 되어 있어서 생략 가능하다. 하지만 다른 파일명의 메시지 파일을 등록하고 싶다면 등록 설정을 지정해야한다.
📌 messages.properties
hello=안녕
good=굳굳
go=이동하기
기본 메시지 파일이다. 한국어를 기본 메시지 파일로 설정했다. 메시지 파일에서는 값을 key와 value 형태로 지정한다. 뷰에서 key 값으로 매칭한다.
📌 messages_en.properties
hello=hi
good=best
go=gogo
영어 메시지 파일이다. 마찬가지로 값을 key와 value 형태로 지정했다.
📌 LocaleResolver 인터페이스
public interface LocaleResolver {
/**
* Resolve the current locale via the given request.
* Can return a default locale as fallback in any case.
* @param request the request to resolve the locale for
* @return the current locale (never {@code null})
*/
Locale resolveLocale(HttpServletRequest request);
/**
* Set the current locale to the given one.
* @param request the request to be used for locale modification
* @param response the response to be used for locale modification
* @param locale the new locale, or {@code null} to clear the locale
* @throws UnsupportedOperationException if the LocaleResolver
* implementation does not support dynamic changing of the locale
*/
void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);
}
먼저 LocaleResolver 인터페이스에 대해서 알아보자.
- resolverLocale() 메서드는 요청과 관련된 Locale을 리턴한다. DispatcherServlet은 등록되어 있는 LocaleResolver의 resolverLocale() 메서드를 호출해서 웹 요청을 처리할 때 사용할 Locale을 구한다.
- setLocale() 메서드는 Locale을 변경할 때 사용된다. 예를 들어 쿠키나 세션에 Locale 정보를 저장할 때 이 메서드가 사용된다.
스프링은 LocaleResolver 인터페이스의 다양한 구현체들을 제공한다.
클래스 | 설명 |
AcceptHeaderLocaleResolver | 웹 브라우저가 전송한 헤더의 Accept-Language 값을 기반으로 Locale을 선택한다. setLocale() 메서드를 지원하지 않는다. |
SessionLocaleResolver | 세션으로부터 Locale 정보를 구한다. setLocale() 메서드는 세션에 Locale 정보를 저장한다. |
CookieLocaleResolver | 쿠키를 이용해서 Locale 정보를 구한다. setLocale() 메서드는 쿠키에 Locale 정보를 저장한다. |
FixedLocaleResolver | 웹 요청에 상관없이 특정한 Locale로 설정한다. setLocale() 메서드를 지원하지 않는다. |
참고로 LocaleResolver를 직접 스프링 빈을 등록할 경우 빈의 이름을 'localeResolver'로 등록해야 한다. LocaleResolver의 구현체가 어떤 것이냐에 따라서 Locale 지정 방식이 정해진다.
AcceptHeaderLocaleResolver 구현
스프링은 언어 선택시 기본으로 HTTP 헤더의 Accept-Language 값을 기반으로 메시지 설정 파일을 선택한다. 스프링 부트는 Accept-Language를 활용하는 AcceptHeaderLocaleResolver를 기본으로 사용한다.
AcceptHeaderLocaleResovler는 HTTP 헤더로부터 Locale 정보를 추출하기 때문에 setLocale() 메서드를 이용해서 Locale 설정을 변경할 수 없다.
먼저 LoclaeResolver 인터페이스를 구현하기 위해 AcceptHeaderLocaleResolver를 스프링 빈으로 등록해보도록 하자.
1. AcceptHeaderLocaleResolver를 스프링 빈에 등록하기
📌 WebConfig
package messages.basicmessagesv2.web;
@Configuration
public class WebConfig {
@Bean
public LocaleResolver localeResolver() {
return new AcceptHeaderLocaleResolver();
}
}
LocaleResolver 인터페이스를 구현하기 위해서 AcceptHeaderLocaleResolver 클래스를 스프링 Bean으로 등록했다. 하지만 이 과정을 생략할 수 있다. 왜냐하면 스프링 부트는 AcceptHeaderLocaleResolver를 기본값으로 사용하고 있기 때문이다.
2. Locale 값 변경하기
AcceptHeaderLocaleResolver는 HTTP 헤더의 Accept-Language 값을 기반으로 Locale 값을 정하기 때문에 Accept-Language 값을 먼저 지정해줘야 한다.
'구글 설정 → 고급 → 언어'로 들어가서 원하는 언어의 우선 순위를 지정하면 된다. 그러면 해당하는 언어에 맞는 메시지 파일이 매칭된다.
SessionLocaleResolver 구현
SessionLocaleResolver는 Http Session에 Locale 정보를 저장한다. setLocale() 메서드를 호출하면 Locale 정보를 세션에 저장하고, resolverLocale() 메서드는 세션으로부터 Locale 값을 가져와서 웹 요청의 Locale을 설정한다. SessionLocaleResolver는 실제로 많이 사용되는 LocaleResolver다.
만약에 Locale 정보가 세션에 존재하지 않으면, defaultLocale 프로퍼티의 값을 Locale로 사용한다. defaultLocale 프로퍼티의 값이 null 인 경우 Accept-Language 헤더의 Loacale 정보를 얻는다.
1. SessionLocaleResolver를 스프링 빈에 등록하기
📌 WebConfig
package messages.basicmessagesv2.web;
@Configuration
public class WebConfig {
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
localeResolver.setDefaultLocale(Locale.KOREA); // 기본 값을 Locale.KOREA로 설정
return localeResolver;
}
}
SessionLocaleResolver#setDefultLocale() 메서드를 통해 기본 언어를 Locale.KOREA로 설정했다. 참고로 Locale.KOREA가 아닌 new Locale("ko")를 파라미터로 전달해도 똑같다. LocaleResolver는 그저 Locale을 결정하는 역할이다.
2. 컨트롤러에서 세션 값 받기
컨트롤러에서 세션 값을 받기 전에 사전 작업들이 필요하다.
🔍 사전 작업
위 사진처럼 select 형식으로 언어를 선택하여 버튼을 누르면 해당 페이지들이 선택한 언어에 맞게 Locale 값이 변경되도록 할 것이다. 그러기 위해선 먼저 select 보기 값에 들어갈 값들을 enum으로 생각하도록 하겠다.
📌 Language
package messages.basicmessagesv2;
@Getter
public enum Language {
BASIC("한국어", Locale.KOREA), ENGLISH("영어", Locale.ENGLISH);;
private String lang;
private Locale locale;
Language(String lang, Locale locale) {
this.lang = lang;
this.locale = locale;
}
}
BASIC의 첫 번째 인자는 뷰의 select 에서 보이는 값이다. 두 번째 인자는 서버로 전달될 Locale 값이다. 이제 뷰에서 Locale 값을 저장할 DTO 객체를 생성하도록 하겠다.
📌 localeDto
package messages.basicmessagesv2.domain;
@Getter @Setter
@NoArgsConstructor
public class LocaleDto {
Locale locale;
}
Locale 정보를 담을 커맨드 객체다. 이제 뷰를
🔍 컨트롤러 작업
먼저 사용자가 선택한 Locale 값이 세션으로 유지 되는지 알아보기 위해 페이지 개수를 2개로 정했다.
📌 BasicController
package messages.basicmessagesv2.controller;
@Controller
@RequiredArgsConstructor
@Slf4j
public class BasicController {
@GetMapping
public String home(@ModelAttribute LocaleDto localeDto, Model model) {
model.addAttribute("languages", Language.values());
return "/home";
}
@PostMapping
public String changeLocale(@ModelAttribute LocaleDto localeDto, HttpSession session) {
Locale locale = localeDto.getLocale();
session.setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, locale);
return "redirect:/";
}
@GetMapping("/test")
public String testPage() {
return "/goTest";
}
}
- 'localhost8080'인 홈 화면을 select로 Locale 값을 설정할 수 있는 페이지로 설정했다.
- 'localhost8080/test' 페이지는 Locale 값이 세션 유지가 되는지 알아보기 위한 페이지다.
- home() 에서 Language Enum을 values() 메서드를 통해 값을 리스트 형태로 넘겼다.
- changeLocale() 에서 클라이언트로부터 LocaleDto를 통해 Locale 값을 받는다. 그리고 HttpSession#setAttribute()를 통해서 Locale 값을 세션에 넣는다. 이 때 세션의 key 값으로는 SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME 으로 지정해야 한다. 그리고 세션의 value 값에는 locale 값으로 넣는다.
- testPage()는 그저 세션에 들어있는 Locale 값이 계속 유지가 되는지 체크하기 위한 페이지다.
여기서 SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME에 대해서 더 알아보기 위해 SessionLocalResolver 클래스를 살펴보자.
📌 SessionLocaleResolver 클래스
public class SessionLocaleResolver extends AbstractLocaleContextResolver {
public static final String LOCALE_SESSION_ATTRIBUTE_NAME = SessionLocaleResolver.class.getName() + ".LOCALE";
private String localeAttributeName = LOCALE_SESSION_ATTRIBUTE_NAME;
public void setLocaleAttributeName(String localeAttributeName) {
this.localeAttributeName = localeAttributeName;
}
...
}
SessionLocaleResolver의 LOCALE_SESSION_NAME은 SessionLocaleResolver.class.getName() + ".LOCALE" 이라는 이름을 갖는다. 이는 패키지를 포함한 이름 뒤에 '.LOCALE' 이라는 세션 Attirbute 이름으로 들어가게 된다.
LOCALE_SESSION_NAME은 static 변수다. 만약에 SessionLocaleResolver의 인스턴스가 존재한다면 LOCALE_SESSION_NAME과 같은 값이면서 인스턴스 변수인 localeAttributeName을 사용해도 된다.
3. 뷰 작업
📌 home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form method="post" th:object="${localeDto}">
<div>언어 선택</div>
<select th:field="*{locale}">
<option value="">== 언어 선택 ==</option>
<option th:each="language : ${languages}" th:value="${language.locale}" th:text="${language.lang}"></option>
</select>
<button type="submit">버튼</button>
</form>
<p th:text="#{hello}"></p>
<a th:href="@{/test}" th:text="#{go}">이동하기</a>
</body>
</html>
- form 태그에 th:object를 통해 localeDto인 커맨드 객체로 값을 받도록 했다. localeDto의 필드 값인 locale을 select 태그에 th:filed 값으로 지정하여 사용자가 선택한 option 값이 locale 값이 된다.
- option 태그에 있는 th:text 값이 선택할 때 사용자한테 보여지는 단어다. th:value 값은 select 태그의 th:field 값에 담겨 서버로 전달될 값이다.
- a태그를 통해서 다른 페이지로 이동할 수 있도록 했다.
홈 화면이 이렇게 보여진다.
📌 goTest.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p th:text="#{good}"></p>
<a th:href="@{/}" th:text="#{go}">이동하기</a>
</body>
</html>
해당 페이지는 세션에 담긴 Locale 값이 유지되나 안되나 확인하기 위한 페이지다. 그러므로 특별한 기능은 없고 메시지 파일의 key값을 th:text로 매칭하여 value 값이 출력되도록 했다.
뷰에서 이렇게 보여진다.
🔍 뷰에서 국제화 기능 확인하기
이제 홈 화면에서 영어를 선택하고 버튼을 누르면 세션에 Locale.ENGLISH 값이 담기고 영문으로 바뀌어야 한다.
영어를 선택하고 버튼을 누른 결과 잘 바뀐 것을 알 수 있다. 이제 Locale 값이 담긴 세션이 그대로 유지되는지 확인하기 위해서 이동하기 버턴을 눌러보자
버튼을 누른 결과 '굳굳' 으로 나왔던 말이 'best'인 영문으로 바뀌어 나온 것을 알 수 있다. Locale 값이 담긴 세션이 잘 유지되고 있다는 것을 확인할 수 있다.
또한 클라이언트에 쿠키 저장소를 보면 세션ID로 값이 잘 들어온 것을 확인할 수 있다.
4. SessionLocaleResolver 검증 코드
📌 BasicControllerTest
package messages.basicmessagesv2.controller;
@SpringBootTest
class BasicControllerTest {
@Autowired private MockHttpSession session;
/**
* 세션에 로케일 값을 저장하고 세션 조회를 통해 로케일 값이 서로 일치하는지 검증
*/
@Test
void GetSessionLocale() {
// given
session.setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, Locale.ENGLISH);
// when
Locale locale = (Locale) session.getAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME);
// then
assertThat(locale).isEqualTo(Locale.ENGLISH);
}
/**
* Locale 값을 저장한 세션 삭제 검증
*/
@Test
void removeSessionLocale() {
// given
session.setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, Locale.ENGLISH);
// when
session.removeAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME);
Locale locale = (Locale) session.getAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME);
// then
assertThat(locale).isNull();
}
}
Mock 객체인 MockHttpSession 객체를 주입 받아서 HttpSession 역할을 구현했다.
이상 SessionLocaleResolver를 통해 국제화 기능 구현을 마친다.
CookieLocaleResolver 구현하기
CookieLocaleResolver는 쿠키를 이용해서 Locale 정보를 저장한다. setLocale() 메서드를 통해 Locale 정보를 담은 Cookie를 생성하고, resolverLocale() 메서드를 통해 Cookie로 부터 Locale 정보를 가져온다.
Locale 정보를 담은 쿠키가 존재하지 않을 경우 defaultLocale 프로퍼티의 값을 Locale 값으로 사용한다. defaultLocale 프로퍼티의 값이 null 값인 경우에는 HTTP 헤더의 Accept-Language 의 Locale 값을 사용한다.
CookieLocaleResolver는 쿠키와 관련한 별도의 설정이 필요 없지만 생성할 쿠키 이름, 도메인, 경로 등의 설정을 직접 할 수있다.
CookieLocaleResolver의 쿠키 설정
프로퍼티 | 설명 | 기본값 |
cookieName | 사용할 쿠키 이름 | classname + locale |
cookieDomain | 쿠키 도메인 | |
cookiePath | 쿠키 경로 | / |
cookieMaxAge | 쿠키 유효 시간 | Integer.MAX_INT |
cookieSecure | 보안 쿠키 여부 | false |
1. CookieLocaleResolver를 스프링 빈에 등록하기
📌 WebConfig
package messages.basicmessagesv2.web;
@Configuration
public class WebConfig {
/**
* CookieLocaleResolver 구현
*/
@Bean
public LocaleResolver localeResolver () {
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setCookieName("lang");
localeResolver.setDefaultLocale(Locale.KOREA);
localeResolver.setCookieHttpOnly(true); // 세션 탈취 방어
}
}
CookieLocaleResolver를 스프링 빈으로 등록했다. setCookieName()으로 쿠키명도 바꿀 수 있고, setDefaultLocale()으로 기본 Locale 설정도 선택할 수 있다.
setCookieHttpOnly() 메소드에 true 파라미터를 전달하면 HttpOnly가 설정된 쿠키가 된다. HttpOnly가 설정된 쿠키는 HTTP 통신 상에만 사용할 수 있다. 즉, 통신에만 사용되므로 자바스크립트 같은 외부 프로그램은 접근할 수 있다. HttpOnly 설정을 통해 외부 프로그램으로부터 세션 탈취를 막을 수 있다.
2. 컨트롤러 작업
컨트롤러 작업 전에 사전 작업을 미리 해야한다. 이 과정은 위의 SessionLocaleResolver 과정에도 똑같이 했으므로 그 부분을 참고하면 된다.
먼저 사용자가 선택한 Locale 값이 쿠키로 유지가 되는지 알아보기 위해 페이지 개수를 2개로 정했다.
📌 CookieLocaleController
package messages.basicmessagesv2.controller;
@Controller
@RequiredArgsConstructor
@RequestMapping("/cookie")
public class CookieLocaleController {
private final LocaleResolver localeResolver;
@GetMapping
public String home(@ModelAttribute LocaleDto localeDto, Model model) {
model.addAttribute("languages", Language.values());
return "/cookie/cookieHome";
}
@PostMapping
public String changeLocale(@ModelAttribute LocaleDto localeDto,
HttpServletRequest request,
HttpServletResponse response) {
Locale locale = localeDto.getLocale();
localeResolver.setLocale(request, response, locale);
return "redirect:/cookie";
}
@GetMapping("/test")
public String testPage() {
return "/cookie/cookieGoTest";
}
}
'localhost8080/cookie' 페이지에서는 select를 통해 CookieLocaleResolver로 Locale 값을 지정할 수 있다.
'localhost8080/cookie/test' 페이지는 해당 페이지에서도 쿠키에 저장된 Locale 값이 유지 되는지 알기 위한 페이지다.
스프링 빈으로 등록한 CookieLocaleResolver를 주입 받아서 LocaleResolver의 setLocale() 메서드를 통해 Locale 값을 지정했다.
3. 뷰 작업
뷰 작업도 위의 SessionLocaleResolver와 똑같으니 해당 과정을 참고할 것. 그저 이동하기 기능의 주소값만 변경해줬다.
📌 cookieHome
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p thtxt>CookieLocaleResolver View</p>
<form method="post" th:object="${localeDto}">
<div>언어 선택</div>
<select th:field="*{locale}">
<option value="">== 언어 선택 ==</option>
<option th:each="language : ${languages}" th:value="${language.locale}" th:text="${language.lang}"></option>
</select>
<button type="submit">버튼</button>
</form>
<p th:text="#{hello}"></p>
<a th:href="@{/cookie/test}" th:text="#{go}">이동하기</a>
</body>
</html>
📌 cookieGoTest
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p>CookieLocaleResolver View</p>
<p th:text="#{good}"></p>
<a th:href="@{/cookie}" th:text="#{go}">이동하기</a>
</body>
</html>
이제 뷰에서 Locale 값을 영어로 변경 했을 때 영문으로 바뀌는지 확인해보자.
영어를 클릭하고 버튼을 누르면 된다.
두 페이지 모두 영어로 잘 나오고 Locale이 저장된 쿠키가 잘 유지 된 것을 확인할 수 있다.
클라이언트의 쿠키 저장소에서도 확인을 해보자.
Locale 값을 한국어로 지정했을 때 쿠키 저장소에 key값으로는 'lang', value 값으로는 'ko-KR'가 담겨 있는 것을 확인할 수 있다.
Locale 값을영어로 지정했을 때 쿠키 저장소에 key값으로는 'lang', value 값으로는 'en'이 담겨 있는 것을 확인할 수 있다.
4. 테스트 코드
package messages.basicmessagesv2.controller;
@SpringBootTest
class CookieLocaleControllerTest {
@Autowired
MockHttpServletResponse response;
@Autowired
MockHttpServletRequest request;
@Autowired
CookieLocaleResolver localeResolver;
@Test
void getCookieLocale() {
// given
localeResolver.setLocale(request, response, Locale.ENGLISH);
// when
Locale locale = localeResolver.resolveLocale(request);
// then
assertThat(locale).isEqualTo(Locale.ENGLISH);
}
}
이상 CookieLocaleResolver를 통해 국제화 기능을 구현하는 것을 마치겠다.
V1 버전 : https://terry9611.tistory.com/290?category=931905
👀 참고자료
https://devbox.tistory.com/entry/Spring-Locale-%EC%B2%98%EB%A6%AC
https://oingdaddy.tistory.com/349
'[ Spring ] > Spring Toy' 카테고리의 다른 글
[Spring] 기본 메시지 기능 V1 (0) | 2022.04.14 |
---|