안녕하세요!
이번에는 메시지 국제화를 살펴보고 프로젝트에 적용한 것들을 포스팅 하겠습니다.
1. i18n 이란?
- "Internationalization"의 축약형이며 단어의 뜻은 "국제화" 입니다.
- 이는 "특정 언어나 지역에 종속되지 않고 다양한 지역, 언어에서 정상 동작하도록 고려하는 개발"을 말합니다
- 서버에서 클라이언트의 응답을 내려줄 때, 응답내용이 특정 지역과 언어에 종속되지 않도록 개발하는 것을 목표로 두었습니다.
2. 필요성
메일 전송
- 프로젝트내에서 사용자에게 메일을 전송하는 기능이 있습니다. (인증 이메일, 비밀번호 재설정 등)
- 메일은 클라이언트의 모국어로 만들어진 메일로 전송되어야 합니다.
- 내국인과 외국인에게 전송되어야 하는 메일 언어가 다릅니다.
예외 메시지
- 로그인 중 잘못된 비밀번호로 로그인 시도를 하였다면, 400 응답과 함께 이에 적절한 메시지를 내려주어야 합니다.
- 이때 클라이언트의 언어로 내려주어야 합니다.
example
- "이메일 혹은 비밀번호가 유효하지 않습니다.
- "The email or password you requested is invalid"
3. Spring Boot에서 구현하기
MessageSource
- 다국어로 작성된 메시지 리소스 파일을 읽어 웹 브라우저의 로케일에 따라 각 언어별 메시지로 설정할 수 있는 컴포넌트입니다.
- 메시지 파일
- key-value 형식의 메시지
- email.notempty=Please provide valid email id.
- email.notempty=Veuillez fournir un identifiant de messagerie valide.
- src/main/resources의 message_${locale}.properties
- key-value 형식의 메시지
4. Database의 Table을 사용한 메시지 번들링
MessageBundle 구현체
ResourceBundleMessageSource
- 자바의 ResourceBundle을 사용해 메시지를 읽습니다.
ReloadableResourceBundleMessageSource
- 자바의 ResourceBundle을 사용해 메시지를 읽습니다.
- 메시지 소스 파일에서 주기적으로 메시지를 다시 읽어 들일 수 있습니다.
DataBase를 선택한 이유
장점
- 다른 팀원들이 메시지 파일을 접근하는데 어려움이 있을꺼라 생각했습니다.
- 그에 비해 DB는 쉽게 접근이 가능합니다.
- 메시지 내용을 추가하거나 수정하려면 서버를 재기동해야 하는 부담이 Database로 관리하는 것보다 크다 생각했습니다.
- ReloadableResourceBundleMessageSource 구현체를 사용할 수 있었으나, 주기적으로 메시지를 다시 읽는 것도 비용이라 생각했습니다.
- 메시지를 가공하고 다른 툴과 연동하는 것은 database의 툴을 사용하는 것이 효과적이라 생각했습니다.
단점
- 쿼리 질의로 인한 오버헤드 발생
- 데이터베이스 형상관리 툴 사용 필요
현재 구동중인 레디스나 JPA 캐싱 기능을 통해 극복 가능하다 생각이 들었고
데이터베이스 형상관리 툴도 서비스가 확장될수록 필수적이라 생각해서
이러한 결정을 내렸습니다.
Language Table
LOCALE
- locale 정보에 대한 컬럼입니다.
MESSAGE_KEY
- 애플리케이션에서 메시지의 키 값에 대한 컬럼입니다.
MESSAGE_PATTERN
- 메시지에 대한 패턴을 저장한 컬럼입니다.
- 파라미터를 받아 메시지를 완성하도록 하기 위해 구성하였습니다.
Unique Key
- LOCALE, MESSAGE_KEY
- LOCALE과 MESSAGE_KEY가 같은 행은 오직 하나여야 합니다.
- 백엔드 개발자 뿐만 아니라 다른 팀원들도 추가할 수 있으므로 제약을 거는 것이 안전하다 생각했습니다.
5. 어려웠던 점
내부 모듈의 의존
스프링 시큐리티나 스프링 validation 모듈의 예외 메시지 접근
- 예외가 발생하면 내부적으로 예외에 대한 메시지 접근을 MessageSource에게 위임합니다.
- 사용자 빈 MessageSource가 모듈의 에러메시지는 메시지 번들러를 찾아가 요청하도록 구현해야 합니다.
구현 방법
- 다국어 메시지 정보가 들어있는 Language 테이블을 접근하는 사용자 빈 MessageSource
- 기본적으로 스프링에서 빈으로 사용하였던 ResourceBundleMessageSource를 의존합니다.
- 커스터마이징한 MessageSource에서 분기처리하여 구현체를 바꾸어 구현하였습니다.
@Configuration
public class MessageSourceConfig {
@Bean
public ResourceBundleMessageSource resourceBundleMessageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
// hibernate : jsr-303 어노테이션 에러 메시지 파일 클래스패스 등록
messageSource.setBasename("org/hibernate/validator/ValidationMessages");
// spring security : 인증, 인가 에러 메시지 파일 클래스패스 등록
messageSource.addBasenames("org/springframework/security/messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
}
@Component
@RequiredArgsConstructor
public class MessageSource extends AbstractMessageSource {
private final ResourceBundleMessageSource resourceBundleMessageSource;
private final LanguageRepository languageRepository;
@Override
protected MessageFormat resolveCode(String key, Locale locale) {
String messagePattern = null;
try {
messagePattern = resourceBundleMessageSource.getMessage(key, null, locale);
} catch (NoSuchMessageException e) {
messagePattern = languageRepository.findByLocaleAndMessageKey(locale.toString(), key)
.map(Language::getMessagePattern)
.orElseThrow(() -> new IllegalStateException("Could not find message pattern"));
}
return new MessageFormat(messagePattern, locale);
}
}
- 먼저 ResourceBundleMessageSource으로 메시지를 조회합니다.
- 존재하지 않을 경우, Language 테이블에서 조회합니다.
6. 사용 예
사용자 예외 처리에 대한 응답 메시지
@ExceptionHandler(value = IllegalArgumentException.class)
public ResponseEntity<ResponseDto<String>> handleIllegalArgumentException(IllegalArgumentException e, Locale locale) {
locale = httpUtils.getLocaleForLanguage(locale);
String messageKey = e.getMessage();
String errorMessage = messageSource.getMessage(messageKey, null, locale);
return httpUtils.createResponseEntity(
httpUtils.createFailureResponse(errorMessage, badRequest.value())
);
}
로그인 실패 응답 메시지
@Component
@RequiredArgsConstructor
public class LoginFailureHandler implements AuthenticationFailureHandler {
private final HttpUtils httpUtils;
private final MessageSource messageSource;
private final LocaleResolver customLocaleResolver;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
Locale locale = customLocaleResolver.resolveLocale(request);
String failureMessage = messageSource.getMessage("login_exception", null, locale);
httpUtils.setResponseWithRedirect(
response,
httpUtils.createFailureResponse(failureMessage, HttpStatus.UNAUTHORIZED.value()),
httpUtils.getCurrentVersionPath("login")
);
}
}
JSR-303 유효성 실패 응답 메시지
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity<ResponseDto<ValidationResult>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
ValidationResult errors = ValidationResult.of(e);
return httpUtils.createResponseEntity(
httpUtils.createFailureResponse(errors, badRequest.value())
);
}
출처
'Spring > Spring Boot' 카테고리의 다른 글
[Spring Boot] 1-2. Spring Boot 사용하기 (1) | 2023.10.07 |
---|---|
[Spring Boot] 1-1. Spring Boot 사용하기 (0) | 2023.10.07 |
[Spring Boot][KoLiving] 3-2 Sign-up (0) | 2023.09.19 |
[Spring Boot][KoLiving] 3-1 Sign-up (0) | 2023.09.19 |
[Spring Boot][KoLiving] 1. Validation (0) | 2023.09.18 |