참고자료:
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의 | 김영한 - 인프런
김영한 | 웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습
www.inflearn.com
메시지
화면에 보이는 어떤 특정 단어를 변경해야 한다면 어떻게 할까? 예를 들어, 상품명이라는 단어를 모두 상품이름으로 변경하고자 하는 요구사항이 들어왔다면 말이다. 이 경우, 모든 파일을 다 뒤져서 상품명을 상품이름으로 바꿔야 할까?
매우 귀찮은 작업이 될 것이다. 이럴때 상품명이라는 단어를 한 곳에서 관리하도록 하고 관리하는 파일에서 상품명을 상품이름으로만 바꿔주면 모든 파일에 적용이 된다면 좋을것이다. 이게 메시지 기능이다.
예를 들어, messages.properties 라는 파일에 메시지 관리용 파일을 만들고,
item=상품
item.id=상품 ID
item.itemName=상품명
item.price=가격
item.quantity=수량
각 HTML들은 다음과 같이 해당 데이터를 key값으로 불러서 사용하는 것이다.
`<label for="itemName" th:text="#{item.itemName}"></label>`
국제화
사용자의 주 언어에 따라 보여지는 서비스의 언어가 달라지게 하는건 웹 애플리케이션이라면 그냥 필수이다. 이 경우도 위 메시지처럼 비슷하게 언어별로 파일을 만들고 관리할 수 있다.
message_en.properties
item=Item
item.id=Item ID
item.itemName=Item Name
item.price=price
item.quantity=quantity
message_ko.properties
item=상품
item.id=상품 ID
item.itemName=상품명
item.price=가격
item.quantity=수량
이런식으로 파일을 분리해서 관리하고 영어를 사용하는 사용자라면 message_en.properties이 파일을 사용하고, 한국어를 사용하는 사용자라면 message_ko.properties이 파일을 사용하면 된다.
한국에서 접근한 것인지 영어권에서 접근한 것인지는 HTTP 헤더에 accept-language 값을 사용하거나, 사용자가 직접 언어를 선택한 것을 쿠키에 저장해서 서버와 통신해서 처리하면 된다. 그리고, 결론적으로 스프링은 기본적인 메시지와 국제화 기능을 모두 제공한다. 그래서 스프링을 사용해서 메시지와 국제화 기능을 알아보자.
스프링 메시지, 국제화 소스 설정
만약, 스프링 부트를 사용하지 않는다면 직접 빈을 등록해야 한다.
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("messages", "errors");
messageSource.setDefaultEncoding("utf-8");
return messageSource;
}
- basenames: 설정 파일의 이름을 지정하는 것이다. 위 코드처럼 복수로 여러개를 지정할 수 있다. 저렇게 여러개로 지정하면 저 이름으로된 파일들을 찾고 그 파일들로 메시지 기능을 사용할 수 있다.
- 파일 위치는 기본이 /resources/messages.properties 이다.
그렇지만, 이 빈을 스프링 부트는 자동으로 등록해준다. 스프링 부트를 사용한다면, 다음과 같이 메시지 소스를 설정할 수 있다.
application.yaml
spring:
messages:
basename: messages
참고로 위 값이 기본값이라 저렇게 작성할거면 작성 안해도 무방하다.
그래서 resources 안에 이렇게 파일 두 개를 만들어보자.
messages.properties
hello=안녕
hello.name=안녕 {0}
messages_en.properties
hello=hello
hello.name=hello {0}
위처럼 작성하면 기본값을 한국어로 설정한 것이다. 만약, messages_en.properties, messages_es.properties, messages.properties 이렇게 파일이 있을 때, 한국어로 설정된 언어 사용자가 진입하면 최초에 _ko를 찾는데 그게 없으면 기본값인 그냥 messages.properties가 적용된다.
스프링 메시지, 국제화 소스 사용
이제 이 메시지 기능을 사용해보자. 간단하게 테스트 코드로 작성해서 확인을 해보자.
`test` 폴더 하위에 테스트 파일 하나를 만들자.
@SpringBootTest
public class MessageSourceTest {
@Autowired
MessageSource messageSource;
}
- 그리고 우선 스프링 부트가 자동으로 빈으로 등록해준 MessageSource를 주입받는다.
@Test
void helloMessage() {
String hello = messageSource.getMessage("hello", null, Locale.US);
assertThat(hello).isEqualTo("hello");
}
- getMessage()를 호출해서 원하는 키를 가져온다. 여기서는 "hello"를 가져온다. 두번째 파라미터는 arguments이다. 이건 이후에 알아본다. 세번째 파라미터는 Locale 값이다. US로 지정해보자.
- 그리고 가져온 값이 "hello"와 일치하는지 테스트한다.
@Test
void notFoundMessage() {
assertThatThrownBy(() -> messageSource.getMessage("no", null, Locale.US)).isInstanceOf(NoSuchMessageException.class);
}
- 이번엔 없는 키를 가져오는 경우 NoSuchMessageException을 받는다. 그것을 테스트한다.
@Test
void defaultMessage() {
String defaultMessage = messageSource.getMessage("no", null, "default Message", Locale.US);
assertThat(defaultMessage).isEqualTo("default Message");
}
- 값이 없는 경우, 기본값을 설정해서 예외를 피할수 있다. 여기서는 "default Message" 라고 기본값을 설정했다.
- 가져온 값이 "default Message"와 일치한지 확인한다.
@Test
void argsMessage() {
String message = messageSource.getMessage("hello.name", new Object[]{"Spring"}, Locale.US);
assertThat(message).isEqualTo("hello Spring");
}
- 이번에는 arguments를 사용해본다. 위에서 `hello.name=hello {0}` 이렇게 messages 파일을 작성한 것이 있다.
- 이 `{0}`에 넘겨진 배열의 0번 인덱스인 "Spring"을집어넣는 것이다.
- 반환값이 "hello Spring"인지 확인한다.
@Test
void localizedMessage() {
String enMessage = messageSource.getMessage("hello", null, Locale.US);
assertThat(enMessage).isEqualTo("hello");
String koMessage = messageSource.getMessage("hello", null, Locale.KOREAN);
assertThat(koMessage).isEqualTo("안녕");
}
- 이번엔 국제화 기능을 사용해본다. Locale.US로 넘긴 경우 messages_en.properties 파일을 찾을것이다.
- Locale.KOREAN을 넘긴 경우 messages_ko.properties 파일을 찾을것이다.
이렇게 스프링의 메시지, 국제화 기능을 코드로써 편리하게 사용할 수 있다.
웹 애플리케이션에 메시지 적용하기
만약, 웹 애플리케이션 개발을 할 때 스프링 부트 + 타임리프를 사용한다면 타임리프에서 스프링의 메시지, 국제화 기능을 바로 적용할 수 있다.
참고로, 앞단을 리액트와 같이 백엔드와 분리하여 개발한다면 앞단에서 처리를 하면 된다.
우선, 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=취소
그리고 타임리프에서 이 값을 그대로 가져다가 사용하면 된다. 아래처럼.
<h2 th:text="#{page.updateItem}">상품 수정 폼</h2>
만약, 파라미터를 줘야하는 `hello.name=hello {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
그럼 끝이다. 왜냐? 메시지 적용하면서 타임리프 템플릿에 이미 필요한 부분에 다음과 같이 적용을 해놨다.
<h2 th:text="#{page.updateItem}">상품 수정 폼</h2>
그래서, 사용자의 브라우저가 어떤 언어를 쓰냐에 따라 알아서 국제화가 적용될 것이다.
그럼 테스트해보자. 일단 브라우저 언어를 바꿔보자.
다시 들어가서 보면 이렇게 영어로 잘 나오게 된다.
LocaleResolver
이걸 살짝만 깊게 들어가보면, 스프링도 결국 Locale 정보를 알아야 언어를 선택할 수 있을 것이다. 그래서 스프링은 언어 선택시 기본으로 Accept-Language의 헤더값을 사용하는데 이게 스프링 부트가 LocaleResolver 인터페이스의 구현체를 기본으로 AcceptHeaderLocaleResolver를 사용하기 때문이다.
LocaleResolver 인터페이스
public interface LocaleResolver {
Locale resolveLocale(HttpServletRequest var1);
void setLocale(HttpServletRequest var1, @Nullable HttpServletResponse var2, @Nullable Locale var3);
}
AcceptHeaderLocaleResolver
package org.springframework.web.servlet.i18n;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;
public class AcceptHeaderLocaleResolver implements LocaleResolver {
private final List<Locale> supportedLocales = new ArrayList(4);
@Nullable
private Locale defaultLocale;
public AcceptHeaderLocaleResolver() {
}
public void setSupportedLocales(List<Locale> locales) {
this.supportedLocales.clear();
this.supportedLocales.addAll(locales);
}
public List<Locale> getSupportedLocales() {
return this.supportedLocales;
}
public void setDefaultLocale(@Nullable Locale defaultLocale) {
this.defaultLocale = defaultLocale;
}
@Nullable
public Locale getDefaultLocale() {
return this.defaultLocale;
}
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = this.getDefaultLocale();
if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
return defaultLocale;
} else {
Locale requestLocale = request.getLocale();
List<Locale> supportedLocales = this.getSupportedLocales();
if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);
if (supportedLocale != null) {
return supportedLocale;
} else {
return defaultLocale != null ? defaultLocale : requestLocale;
}
} else {
return requestLocale;
}
}
}
@Nullable
private Locale findSupportedLocale(HttpServletRequest request, List<Locale> supportedLocales) {
Enumeration<Locale> requestLocales = request.getLocales();
Locale languageMatch = null;
Locale locale;
label38:
do {
while(requestLocales.hasMoreElements()) {
locale = (Locale)requestLocales.nextElement();
if (supportedLocales.contains(locale)) {
continue label38;
}
if (languageMatch == null) {
Iterator var6 = supportedLocales.iterator();
while(var6.hasNext()) {
Locale candidate = (Locale)var6.next();
if (!StringUtils.hasLength(candidate.getCountry()) && candidate.getLanguage().equals(locale.getLanguage())) {
languageMatch = candidate;
break;
}
}
}
}
return languageMatch;
} while(languageMatch != null && !languageMatch.getLanguage().equals(locale.getLanguage()));
return locale;
}
public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) {
throw new UnsupportedOperationException("Cannot change HTTP accept header - use a different locale resolution strategy");
}
}
그 말은 우리가 LocaleResolver 구현체를 다른것으로 변경한다면, 얼마든지 Locale 정보를 다른 방식으로 알아올 수 있다. 가장 대표적인 예시가 잘 만든 웹 사이트를 보면 사이트 자체적으로 언어를 선택할 수 있는 사이트가 있다. 이렇게 사용자가 브라우저 단위가 아니라 웹 애플리케이션 단위로 언어를 선택했을 때 그 값을 쿠키같은 곳에 저장하고 서버와 통신하게 하고 Locale 정보를 내가 직접 만든 LocaleResolver 구현체를 쿠키에서 가져와서 Locale값을 알아내는 구현체를 만들면 된다.
그리고? 그 구현체를 빈으로 등록하면 스프링은 내가 만든 구현체로 변경해줄것이다. 이게 바로 스프링의 위대함이다. 얼마든지 구현체만 갈아끼우면 된다는 것. 이게 바로 DI의 핵심이다. 하나의 인터페이스에 여러 구현체. 의존관계 설정을 나중으로 미루는 것. 확장에는 열려있고 변경에는 닫혀있는 OCP 법칙.
아무튼, 이렇게 스프링, 스프링 부트를 이용해서 메시지, 국제화 기능을 알아보았다!
'Spring MVC' 카테고리의 다른 글
데이터 검증2 (Bean Validation으로 @ModelAttribute, @RequestBody 객체를 검증) (4) | 2024.09.01 |
---|---|
데이터 검증 (BindingResult, Validator, @Validated) (0) | 2024.08.31 |
Spring MVC 사용하기 Part.3 (0) | 2024.08.02 |
Spring MVC 사용하기 Part.2 (0) | 2024.07.11 |
Spring MVC 사용하기 Part.1 (0) | 2024.07.08 |