현재 개발 중인 어드민 사이트는 다국어 지원이 되어야 한다.
내가 맡은 부분은 서버 즉 백엔드 부분이므로, 설정한 언어에 맞게 Error Message를 클라이언트로 전달해야 했다.
기존 구 어드민 사이트에서는 .properties 파일에 Message들을 저장했지만 나는 DB에 저장하여 관리키로 함.
전략이라 할 것도 없지만.. 어쨌든 내가 가고자 하는 방향에 맞는 전략은 아래와 같다.
(내 전략의 방향이 맞는지, 그리고 이것을 구현한 방법이 맞는지 아직도 의문이다. 따라서 혹시라도 이것을 보시고 더 좋은 방법이나 틀린 부분이 있다고 느끼시는 분들이 있다면 Comment로 남겨주시면 제가 그것을 감사히 읽어보고 수정하도록 하겠습니다.)
- 에러 메시지는 DB에 저장
- 에러 메시지를 클라이언트로 전달할 때는 Caching되어진 데이터를 전달하고, 데이터는 locale과 messageKey 값으로 찾음
- Exception은 @RestControllerAdvice로 지정한 class에서 처리
@RestContorllerAdvice로 설정된 전역 ExceptionHandler가 클라이언트에서 전달한 Accept-Language Header값에 맞는 Exception Message로 전달하는 역할을 담당한다. 각 메소드는 Exception에 대한 MessageKey를 가지고 있어 Caching되어진 Exception Data에서 MessageKey와 Locale 정보에 맞는 Exception Message를 불러와서 Client로 전달한다. 단, 어떤 Exception 은 구체화된 에러 메시지를 내려줄 필요성이 있기에 이런 경우에는 exception을 throw할 때 overriding한 CustomException의 messageKey를 set해서 전달한다. 아래 로직은 @ExceptionHandler가 Exception을 Client에 전달하기 전에 수행하는 작업에 대한 내용이다.
@RestControllerAdvice
@Slf4j
public class AdminExceptionHandler {
@ExceptionHandler(ForbiddenRequestException.class)
public ResponseEntity<?> handleForbiddenRequestException(ForbiddenRequestException e, Locale locale) {
log.error(ExceptionUtils.getExceptionMessage(e));
String messageKey;
if (e.getMsgKey() != null) {
messageKey = e.getMsgKey();
} else {
messageKey = "forbidden.request";
}
return ApiMessage.builder().errorMessage(resource.getMessage(CacheKeys.builder().messageKey(messageKey).locale(locale.toString()).build())).status(e.getStatus()).build().toEntity();
}
}
그리고 아래 부분은 DefaultLocale 값을 설정하고, Accept-Language 값을 Locale로 설정하기 위해 구현한 내용이다.
나는 CookieLocaleResolver를 사용했다. 아래 설정을 마치면 @ExceptionHandler로 지정한 메소드로 전달한 Locale값이 Accept-Language 헤더값으로 잘 전달된다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Arrays;
import java.util.Locale;
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setDefaultLocale(Locale.KOREA);
return localeResolver;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("lang");
return lci;
}
}
다국어 데이터를 Caching 하여 필요할 때 꺼내쓰려면 messageKey와 locale 두개의 값이 Caching 데이터의 Key가 되어야 했고 이를 위해 아래 CacheKeys라는 아래 class를 만들었다.
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.Objects;
@Getter
@Setter
@Builder
public class CacheKeys implements Serializable {
private String messageKey;
private String locale;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CacheKeys cacheKeys = (CacheKeys) o;
return Objects.equals(messageKey, cacheKeys.messageKey) &&
Objects.equals(locale, cacheKeys.locale);
}
@Override
public int hashCode() {
return Objects.hash(messageKey, locale);
}
}
getMessage 메소드가 캐싱하는 대상 메소드이다.
보시다시피 key는 위에 정의한 CacheKey 클래스의 messageKey와 locale 값이다.
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
import java.util.List;
@Slf4j
@Component
public class ErrorMessageComponent {
@Autowired
ErrorMsgI18nRepository errorMsgI18nRepository;
@Cacheable(value = "errorMsgCache", key = "#cacheKeys")
public String getMessage(CacheKeys cacheKeys) {
log.debug("by getMessage method -> not by cache");
return errorMsgI18nRepository.findByMsgKeyAndLocale(cacheKeys.getMessageKey(), cacheKeys.getLocale()).orElse("CACHE KEYS ERROR");
}
}
class가 key로 되게 하기 위해서는 우선 내가 사용한 ehcache의 xml파일에 정의를 해주어야 한다.
key가 바로 CacheKeys라는 것을 명시해주고, value는 String임을 정의해준다.
<expiry>를 보면 서비스가 run하는 중에는 만료되지 않게 하기 위해 <none/>를 해준 것을 알 수 있다.
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.ehcache.org/v3"
xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
xsi:schemaLocation="
http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">
<cache alias="errorMsgCache">
<key-type>com.test.entity.CacheKeys</key-type>
<value-type>java.lang.String</value-type>
<expiry>
<none/>
</expiry>
<resources>
<heap unit="entries">10</heap>
<offheap unit="MB">10</offheap>
</resources>
</cache>
</config>
- Ehcache
- Hibernate와 캐시 제공자 사이의 다리 역할
- redis / memchached와 달리 별도 서버를 가지지 않으며, Spring 내부적으로 동작하여 캐싱함
- 캐시하고 싶은 메소드에 @Cacheable(value = "xxx")를 붙여주면 된다.
- Second-Level Cache란?
- 세션 단위의 1차 레벨 캐시라는 개념을 적용한게 Hibernate / 세션이 종료되면 First-Level Cache도 종료
- Second-Level Cache는 SessionFactory 범위이므로 모든 세션에서 같은 SessionFactory를 공유한다.
- 엔티티가 Second-Level Cahing 활성화 상태가 되면?
- 인스턴스가 First-Level-Cache에 있는 경우엔 First-Level-Cache에서 반환된다.
- First-Level-Cache에서 인스턴스를 찾을 수 없고 해당 인스턴스 상태가 Second-Level에 Cache된 경우 거기서 데이터를 가져온다.
- 그렇지 않으면 필요한 데이터가 데이터베이스에서 로드되고 Instance가 assemble되고 반환된다.
- Second-Level Cache란?
일정에 쫓기어 글을 완성도 있게 마무리 하고 싶었지만 우선 이정도로 마무리..
언제쯤이면 일도 척척 하고 포스팅도 여유롭게 할 수 있는 경지에 다다를 수 있을런지
얼른 끝내고 다시 돌아오겠습니다~
참고.
'스프링' 카테고리의 다른 글
Spring Data JPA / Dynamic DataSource 설정 (0) | 2022.03.28 |
---|---|
OSIV / @Transactional / Multi Datasource (0) | 2022.03.08 |
Spring vs Spring Boot 차이점 (0) | 2021.08.09 |
SpringBoot Maven 프로젝트 Code Pipeline으로 자동빌드&배포 (0) | 2020.05.08 |
Spring의 기본개념과 동작원리 + 자기반성 (0) | 2020.04.29 |