Spring Boot

SpringBoot를 활용한 Rest api 만들기(7) - MessageSource를 이용한 Exception 처리

ZIAHO 2023. 3. 9. 22:19

이번 포스팅에서는 Spring에서 메시지를 처리하는 방법에 대해서 알아보고, MessageSource를 이용하여 Exception Message를 고도화 해보도록 하겠습니다.

 

Spring에서는 다국어를 처리하기 위해 i18n 세팅을 지원하고 있습니다.

i18n이 무엇이냐면 국제화(Internationalization)의 약자입니다.(I + 가운데 남은 글자 수 + n)

해당 세팅을 통해 안국어로 "안녕하세요"를 영문권에서는 "Hello"로 표시되도록 할 수 있습니다. 이 방법을 이용하여 예외시 메시지 처리 방식을 변경시켜보도록 하겠습니다.


message properties를 yml로 작성하기 위한 라이브러리 추가

Spring 기본 설정에서 실제 다국어 메시지가 저장되는 파일은 'message_ko.properties', message_en.properties'와 같은 형식으로 .properties 파일에 저장됩니다. 그렇지만 여기서는 환경 설정 파일인 application.yml처럼 yml의 장점을 살려 메시지 파일을 저장하려고 합니다.

그러기 위해서는 아래의 라이브러리 추가가 필요합니다.

akkinoc/yaml-resource-bundle: Java ResourceBundle for YAML format. (github.com)

 

GitHub - akkinoc/yaml-resource-bundle: Java ResourceBundle for YAML format.

Java ResourceBundle for YAML format. Contribute to akkinoc/yaml-resource-bundle development by creating an account on GitHub.

github.com

build.gradle 파일의 dependencies에 다음을 추가합니다.

// message properties를 yml로 작성하기 위한 라이브러리 추가
implementation("net.rakugakibox.util:yaml-resource-bundle:1.1")

MessageConfiguration 파일 생성

org.ziaho.ziahorestapi.config 하위에 MessageConfiguration 을 생성합니다.

Spring에서 제공하는 LocaleChangeInterceptor를 사용하여 lang이라는 RequestParameter가 요청에 있으면 해당 값을 읽어 Locale정보를 변경 합니다.

아래에서 로케일 정보는 기본으로 Session에서 읽어오고 저장하도록 SessionLocaleResolver를 사용하였는데, 아래와 같이 다른 리졸버들도 있으므로 상황에 따라 적절한 리졸버를 설정하여 사용하시면 됩니다.

org.springframework.web.servlet.i18n (Spring Framework 6.0.6 API)

 

org.springframework.web.servlet.i18n (Spring Framework 6.0.6 API)

Provides servlets that integrate with the application context infrastructure, and the core interfaces and classes for the Spring web MVC framework.

docs.spring.io

  • AbstractLocaleContextResolver
  • AbstractLocaleResolver
  • AcceptHeaderLocaleResover
  • CookieLocaleResolver
  • FixedLocaleResolver
  • SessionLocaleResolver
package org.ziaho.ziahorestapi.config;

import net.rakugakibox.util.YamlResourceBundle;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

@Configuration
public class MessageConfiguration implements WebMvcConfigurer {

    @Bean // 세션에 지역 설정. default는 KOREAN = 'ko'
    public LocaleResolver localeResolver() { // 지역 설정
        SessionLocaleResolver slr = new SessionLocaleResolver();
        slr.setDefaultLocale(Locale.KOREAN);
        return slr;
    }

    @Bean // 지역설정을 변경하는 인터셉터. 요청시 파라미터에 lang 정보를 지정하면 변경됨
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
        lci.setParamName("lang");
        return lci;
    }

    @Override // 인터셉터를 시스템 레지스트리에 등록
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Bean // yml 파일을 참조하는 MessageSource 선언
    public MessageSource messageSource (
            @Value("${spring.messages.basename}") String basename,
            @Value("${spring.messages.encoding}") String encoding
    ) {
        YamlMessageSource ms = new YamlMessageSource();
        ms.setBasename(basename);
        ms.setDefaultEncoding(encoding);
        ms.setAlwaysUseMessageFormat(true);
        ms.setUseCodeAsDefaultMessage(true);
        ms.setFallbackToSystemLocale(true);
        return ms;
    }

    // locale 정보에 따라 다른 yml 파일을 읽도록 처리 (ko, en)
    private static class YamlMessageSource extends ResourceBundleMessageSource {
        @Override
        protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
            return ResourceBundle.getBundle(basename, locale, YamlResourceBundle.Control.INSTANCE);
        }
    }
}

위처럼 소스를 작성하다 보면 YamlMessageSource를 찾을 수 없는 오류가 발생한다.

implementation("net.rakugakibox.util:yaml-resource-bundle:1.1")

를 사용하지만 해당 라이브러리가 버전 업이 되었고, 그로 인해서 사용 할 수 없는 것으로 보인다.

 

따라서 springframework 기반의 properties를 사용하여 실습을 진행하도록 하겠다.

오류 해결 출처

springboot로 Rest api 만들기(7) MessageSource를 이용한 Exception 처리 (tistory.com)

 

springboot로 Rest api 만들기(7) MessageSource를 이용한 Exception 처리

시작 전 변경 사항이 있다! (2022.07.18 수정) https://github.com/akkinoc/yaml-resource-bundle/issues/103 bug: No libraries found for 'dev.akkinoc.util.YamlResourceBundle' · Issue #103 · akkinoc/yaml-resource-bundle Describe the bug use Gradle G

pepega.tistory.com


수정된 MessageConfiguration 파일

package org.ziaho.ziahorestapi.config;

import java.util.Locale;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

@Configuration
public class MessageConfiguration {

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
        sessionLocaleResolver.setDefaultLocale(Locale.KOREA);
        return sessionLocaleResolver;
    }
}

application.yml i18n 경로 추가

spring:
  messages:
    basename: i18n/exception

다국어 처리 message properties 파일 작성

resource 아래에 i18n 디렉토리를 생성하고 exception_en.properties, exception.properties을 생성하고 다음과 같이 입력합니다.

참고로 메시지 값은 모두 String으로 정의해야 합니다.

# exception_en.properties

unKnown.code = -9999
unKnown.message = An unknown error has occurred.

userNotFound.code = -1000
userNotFound.message = This member not exist.
# exception.properties

unKnown.code = -9999
unKnown.message = 알 수 없는 오류가 발생하였습니다.

userNotFound.code = -1000
userNotFound.message = 존재하지 않는 회원입니다.

ResponseService의 getFailResult method가 code, msg를 받을 수 있도록 수정

// 실패 결과만 처리하는 메소드
public CommonResult getFailResult(int code, String message) {
    CommonResult result = new CommonResult();
    result.setSuccess(false);
    result.setCode(code);
    result.setMsg(message);
    return result;
}

ExceptionAdvice의 에러 메시지를 messageSource 내용으로 교체

package org.ziaho.ziahorestapi.advice;

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.ziaho.ziahorestapi.advice.exception.CUserNotFoundException;
import org.ziaho.ziahorestapi.model.response.CommonResult;
import org.ziaho.ziahorestapi.service.ResponseService;

@RequiredArgsConstructor
@RestControllerAdvice // 예외 발생 시, Json의 형태로 결과를 반환하기 위함
public class ExceptionAdvice {

    private final ResponseService responseService;
    private final MessageSource messageSource;

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    protected CommonResult defaultException(HttpServletRequest request,
                                            Exception e) {
        return responseService.getFailResult(Integer.parseInt(getMessage("unKnown.code")),
                getMessage("unKnown.message")
        );
    }

    @ExceptionHandler(CUserNotFoundException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    protected CommonResult userNotFoundException(HttpServletRequest request, CUserNotFoundException e) {
        return responseService.getFailResult(
                Integer.parseInt(getMessage("userNotFound.code")),
                getMessage("userNotFound.message")
        );
    }

    // code 정보에 해당하는 메시지를 조회합니다.
    private String getMessage(String code) {
        return getMessage(code, null);
    }

    // code 정보, 추가 argument로 현재 locale에 맞는 메시지를 조회합니다.
    private String getMessage(String code, Object[] args) {
        return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
    }

}

결과 확인

999번 회원을 조회하려고 하자, 해당하는 데이터가 없기 때문에 설정해 둔 code와 msessage로 에러코드가 발생했습니다.

 

영문 에러 메시지도 확인하기 위해 UserController에서 lang 정보를 받을 수 있도록 수정합니다.

@Operation(summary = "회원 단건 조회", description = "msrl로 회원을 조회한다.")
@GetMapping(value = "/user/{msrl}")
public SingleResult<User> findUserById(@Parameter(name = "회원번호", required = true) @PathVariable long msrl,
                                       @Parameter(name = "언어") @RequestParam(value = "lang") String lang) throws Exception {
    // 결과 데이터가 단일건인 경우 getSingleResult를 이용하여 결과를 출력
    return responseService.getSingleResult(userJpaRepo.findById(msrl).orElseThrow(CUserNotFoundException::new));

쿼리스트링으로 lang=en을 넘겨주었지만 적용이 안된다,,,,,

 

찾아보니 MessageConfiguration쪽에서 넘겨주는 파라미터에 맞는 properties파일을 찾도록 설정을 해 주어야 하는것 같은데 수정을 해보아도 역시나 적용이 되지 않는다,,,

 

더 찾아보고 수정 할 예정



출처

SpringBoot2로 Rest api 만들기(7) – MessageSource를 이용한 Exception 처리 (daddyprogrammer.org)

 

SpringBoot2로 Rest api 만들기(7) – MessageSource를 이용한 Exception 처리

이번 시간에는 Spring에서 메시지를 처리하는 방법에 대해 알아보고, MessageSource를 이용하여 Exception Message를 고도화해 보도록 하겠습니다. Spring에서는 다국어를 처리하기 위해 i18n 세팅을 지원하

www.daddyprogrammer.org

 

codej99/SpringRestApi at feature/messagesource (github.com)

 

GitHub - codej99/SpringRestApi: SpringBoot2, SpringSecurity, JWT, Stateless Restful API

SpringBoot2, SpringSecurity, JWT, Stateless Restful API - GitHub - codej99/SpringRestApi: SpringBoot2, SpringSecurity, JWT, Stateless Restful API

github.com