LocaleResolver를 사용하면, API를 처리하는 로직에서 다국어를 처리할 수 있다.
쿠키를 사용하는 CookieLocaleResolver의 경우 timezone, locale을 지원해 준다. 하지만, Accept-Language를 사용할 경우 AcceptHeaderLocaleResolver를 사용하는데, AcceptHeaderLocaleResolver는 locale만 지원한다.
들어가기 전
본 포스팅은 Spring Boot3기반으로 작성했습니다. 전체코드는 깃허브를 참고해 주세요.
https://github.com/Dove-kim/spring-boot-3-localization
Timezone.. 이 Header에 필요한가요?
한국계 미국인이 아랍권 국가에서 서비스를 사용할 경우 다음과 같이 정보를 생각할 수 있다.
1. 언어 - 한국어
2. 정책 - 미국
3. time zone - 아랍
1. App이 호출 할 때 쿠키를 사용하는 건 어떨까?
CookieLocaleResolver를 사용하면 timezone을 받을 수 있으나, 웹이 아닌 클라이언트도 쿠키를 세팅해야 하는 기이한 현상이 발생한다. 쿠키도 결국 헤더에 Set-Cookie로 값을 세팅하니, 쿠키의 원형은 결국 Header다.
플랫폼 간 API호출을 통일화하기 위해 쿠키보단 Header를 이용하는 것이 바람직해 보인다.
2. 클라이언트가 타임존을 확인해서 계산은 건 어떨까?
괜찮은 방법이다. 이건 개발 시작전 인터페이스를 협의할 때 클라이언트 개발자와 잘 협의하면 해결된다.
클라이언트에서 시간을 타임존에 계산할 경우 서버는 unix timestamp나 UTC+00:00 기준 시간을 제공하고 클라이언트가 계산을 한다.
하지만 일부 레거시 서비스의 경우 클라이언트의 수정이 여러가지 원인에 의해 불가해서 서버가 계산을 해 줘야 할 수 있다.
즉, 기본적으로 서버는 기준시간을 제공하여 클라이언트가 timezone을 계산하되 일부 API에서는 Header에 Timezone을 받을 수 있게 구현하는 걸 고려해 볼 수 있다.
LocaleResoler를 이용하면, 서비스 레이어에서 필요할 경우 timezone을 사용할 수 있다.
Header를 이용해 timezone을 받도록 세팅하기 위해 CookieLocaleResolver를 먼저 확인해 보자
CookieLocaleResolver 내부는 Header와 무엇이 다른가?
CookieLocaleResolver는 AbstractLocaleContextResolver를 상속받았고, AcceptHeaderLocaleResolver는 AbstractLocaleResolver를 상속받았다.
AbstractLocaleContextResolver는 AbstractLocaleResolver를 상속받고 timezone을 추가했다.
그러면 AbstractLocaleContextResolver를 상속받아서 특정 Header값을 timezone으로 매핑하고 LocaleContext로 세팅하면 될 듯하다.
구현해 보자
public class AcceptHeaderLocaleAndTimeZoneResolver extends AbstractLocaleContextResolver {
public static final String LOCALE_HEADER_ATTRIBUTE_NAME = "Accept-Language";
public static final String TIME_ZONE_HEADER_ATTRIBUTE_NAME = "Accept-TimeZone";
@Override
public LocaleContext resolveLocaleContext(final HttpServletRequest request) {
Locale locale = resolveLocale(request);
TimeZone timeZone = resolveTimeZone(request);
return new TimeZoneAwareLocaleContext() {
@Override
@Nullable
public Locale getLocale() {
return locale;
}
@Override
@Nullable
public TimeZone getTimeZone() {
return timeZone;
}
};
}
@Override
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = getDefaultLocale();
String acceptLanguage = request.getHeader(LOCALE_HEADER_ATTRIBUTE_NAME);
if (StringUtils.hasText(acceptLanguage)) {
return Locale.forLanguageTag(acceptLanguage);
} else {
return (defaultLocale != null) ? defaultLocale : request.getLocale();
}
}
private TimeZone resolveTimeZone(HttpServletRequest request) {
String timeZoneHeader = request.getHeader(TIME_ZONE_HEADER_ATTRIBUTE_NAME);
if (StringUtils.hasText(timeZoneHeader)) {
try {
ZoneId zoneId = ZoneId.of(timeZoneHeader);
return TimeZone.getTimeZone(zoneId);
} catch (DateTimeException e) {
// 유효하지 않은 timeZoneHeader 값을 무시하고 기본 TimeZone을 설정
return getDefaultTimeZone() != null ? getDefaultTimeZone() : TimeZone.getDefault();
}
} else {
return getDefaultTimeZone() != null ? getDefaultTimeZone() : TimeZone.getDefault();
}
}
@Override
public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable LocaleContext localeContext) {
throw new UnsupportedOperationException("Cannot change HTTP headers - use a different locale resolution strategy");
}
@Override
public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) {
throw new UnsupportedOperationException("Cannot change HTTP Accept-Language header - use a different locale resolution strategy");
}
}
만든 구현체를 Bean으로 등록하면 끝난다.
@Configuration
@EnableWebMvc
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
@Bean
public LocaleResolver localeResolver() {
return new AcceptHeaderLocaleAndTimeZoneResolver();
}
}
테스트 코드도 작성해 보자
@WebMvcTest(MainController.class)
class MainControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void givenAcceptTimeZoneHeader_whenResolveTimeZone_thenTimeZoneIsSet() throws Exception {
// Given
String acceptTimeZone = "Europe/Paris";
TimeZone expectedTimeZone = TimeZone.getTimeZone(acceptTimeZone);
// When
String response = mockMvc.perform(get("/").header("Accept-TimeZone", acceptTimeZone))
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString();
// Then
Assertions.assertEquals(response, expectedTimeZone.getID());
}
}
Spring Boot 2.1부터는 이미 정의한 Bean의 Overrideing을 불허한다..?
Resolver를 등록하면 위 스크린샷처럼 에러가 발생한다.
The bean 'localeResolver', defined in class path resource [com/dove/config/WebConfig.class], could not be registered. A bean with that name has already been defined in class path resource [org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.class] and overriding is disabled.
Spring Boot 2.1부터 스프링은 이 정의된 Bean의 오버라이딩을 막았다.
이를 해결하기 위해 오버라이딩을 허용해 준다.
spring:
main:
allow-bean-definition-overriding: true
참고한 글
'개발 고민' 카테고리의 다른 글
JPA IdentifierGenerator의 connection에 대해.. (0) | 2023.09.04 |
---|---|
단일 책임 원칙과 클린코드 V2 (0) | 2023.08.09 |
MultipartFile 업로드에 관하여 (0) | 2023.05.30 |