• [ 배경 ]
    • Spring에서 다국어를 처리하기 위해 찾아보는 중 i18n 설정을 지원하고 있었습니다. i18n의 i는 Internationalization의 약자입니다. i와 n사이에 18개의 알파벳이라는 뜻입니다.
      즉 해당 세팅을 통해 한국어를 영문권에서도 표시할 수 있도록 할 수 있습니다. 해당 방법을 이용해 예외 처리 메시지를 다국어를 적용시켜보겠습니다.

  • [ 과정 ]
    • pom.xml 에 yaml-rsource-bundle 을 추가합니다. 해당 의존성은 YamlResourceBundle클래스를 사용할 수 있고
      그로 인해 실제 언어 정보가 담겨있는 exception_ko.yml, exception_en.yml 파일을 사용할 수 있습니다.
    • <dependency>
      <groupId>net.rakugakibox.util</groupId>
      <artifactId>yaml-resource-bundle</artifactId>
      <version>1.1</version>
      </dependency>
      view raw pom.xml hosted with ❤ by GitHub
    • MessageConfiguration 파일 생성
      • com.market.server.config 패키지 하위에 클래스를 생성 후 @Configuration 어노테이션을 통해 환경 구성을 합니다. 설정한 내용으로는 아래와 같습니다.
        • 세션에 Default 지역 정보를 설정
        • 요청 시 파라미터에 lang 정보를 지정하여 LocalechangeInterceptor를 통해 언어가 변경 되게 설정
        • yml 파일을 참조하는 MessageSource를 설정
        • locale 정보에 따른 다른 yml파일을 읽도록 처리
        • 인터셉터를 시스템 레지스트리에 등록 
        • /*
          어노테이션기반 환경구성을 돕는다. 이 어노테이션을 구현함으로써 클래스가 하나 이상의
          @Bean 메소드를 제공하고 스프링 컨테이가 Bean정의를 생성하고 런타임시 그 Bean들이
          요청들을 처리할 것을 선언하게 된다. 아래는 자바 클래서에서 어노테이션을 포함하는 방법을 보여준다.
          */
          @Configuration
          public class MessageConfig 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 파일을 읽도록 처리
          private static class YamlMessageSource extends ResourceBundleMessageSource {
          @Override
          protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
          return ResourceBundle.getBundle(basename, locale, YamlResourceBundle.Control.INSTANCE);
          }
          }
          }
      • (참고) @Configuration 어노테이션은 @Bean 메서드를 제공하고 스프링 컨테이너가 Bean정의를 생성하고 런타임 시 Bean들이 요청을 처리할 수 있게 해주는 어노테이션입니다.
      • 스프링에서는 LocaleChangeInterceptor를 사용하여 lang이라는 RequestParameter가 요청에 있으면 해당 값을 읽어 로케일 정보를 변경합니다. 중고거래 프로젝트에서는 Session에서 읽어오고 저장하도록 SessionLocaleResolver를 사용하였습니다.
      • Session 말고도 Http요청 시 헤더에 기본 로케일 설정, 쿠기를 사용하는 방식 등을 사용하는 Resolver 도 있습니다. 다른 Resolver 내용이 궁금하신 분은 아래 사이트를 참조 부탁드립니다.
    • application.properties에 다국어 처리 yml i18n 경로와 인코딩 정보를 추가합니다.
    • # message
      spring.messages.basename=i18n/exception
      spring.messages.encoding=UTF-8
    • 다국어 처리 message yml을 형식에 맞게 작성합니다. exception_en.yml, exception_ko.yml 파일을
      생성 후 code, 국가 언어에 맞는 msg 정보를 아래와 같이 추가합니다.
    • # exception_en.yml
      unKnown:
      code: "-9999"
      msg: "An unknown error has occurred."
      userNotFound:
      code: "-1000"
      msg: "This member not exist"
      # exception_ko.yml
      unKnown:
      code: "-9999"
      msg: "알수 없는 오류가 발생하였습니다."
      userNotFound:
      code: "-1000"
      msg: "존재하지 않는 회원입니다."
    • ResposeService에서 서버가 클라이언트에게 응답할 때 response 패킷을 정의합니다.
      성공, 실패 처리 시에 해당하는 code, msg를 response패킷에 설정합니다.
    • // 해당 Class가 서비스(서버에서 클라이언트에게 응답할때 response 패킷을 정의) 레이어 클래스라는 것을 spring framework에 정의한다.
      // response를 class로 분리한 이유는 해당 클래스를 사용하는 곳에서 ExceptionHandler의 형태를 직접 정의한다.
      @Service
      public class ResponseService {
      // enum으로 api 요청 결과에 대한 code, message를 정의합니다.
      public enum CommonResponse {
      SUCCESS(0, "성공하였습니디."),
      FAIL(-1, "실패하였습니다.");
      int code;
      String msg;
      CommonResponse(int code, String msg) {
      this.code = code;
      this.msg = msg;
      }
      public int getCode() {
      return code;
      }
      public String getMsg() {
      return msg;
      }
      }
      // 단일건 결과를 처리하는 메소드
      public <T> SingleResult<T> getSingleResult(T data) {
      SingleResult<T> result = new SingleResult<>();
      result.setData(data);
      setSuccessResult(result);
      return result;
      }
      // 다중건 결과를 처리하는 메소드
      public <T> ListResult<T> getListResult(List<T> list) {
      ListResult<T> result = new ListResult<>();
      result.setList(list);
      setSuccessResult(result);
      return result;
      }
      // 성공 결과만 처리하는 메소드
      public CommonResult getSuccessResult() {
      CommonResult result = new CommonResult();
      setSuccessResult(result);
      return result;
      }
      // 실패 결과만 처리하는 메소드
      public CommonResult getFailResult(int code, String msg) {
      CommonResult result = new CommonResult();
      result.setSuccess(false);
      result.setCode(code);
      result.setMsg(msg);
      return result;
      }
      ...생략...
      }
    • @RestControllerAdvice 어노테이션을 사용하는 ExceptionAdvice 클래스를 통해 Controller에서 발생하는 예외를 ExceptionHandler를 통해 Response에 code값과 msg값을 연결합니다.
    • /*
      이 어노테이션은 초기화 되지않은 final 필드나,
      @NonNull 이 붙은 필드에 대해 생성자를 생성해 줍니다.
      주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용되곤 합니다.
      */
      @RequiredArgsConstructor
      /*
      ControllerAdvice의 annotation은 @ControllerAdvice @RestControllerAdvice 두가지가 있습니다. 예외 발생 시 json형태로 결과를 반환하려면
      @RestControllerAdvice를 클래스에 선언하면 됩니다. annotation에 추가로 패키지를 적용하면 위에서 설명한 것처럼 특정 패키지 하위의 Controller에만 로직이 적용되게도 할 수 있습니다.
      ex) @RestControllerAdvice(basePackages = “com.rest.api”)
      아무것도 적용하지 않으면 프로젝트의 모든 Controller에 로직이 적용됩니다.
      */
      @RestControllerAdvice
      public class ExceptionAdvice {
      private final ResponseService responseService;
      private final MessageSource messageSource;
      /*
      Exception이 발생하면 해당 Handler로 처리하겠다고 명시하는 annotation입니다. 괄호안에는 어떤 Exception이 발생할때 handler를 적용할 것인지
      Exception Class를 인자로 넣습니다. 예제에서는 Exception.class를 지정하였는데 Exception.class는 최상위 예외처리 객체이므로 다른
      ExceptionHandler에서 걸러지지 않은 예외가 있으면 최종으로 이 handler를 거쳐 처리됩니다. 그래서 메서드 명도 defaultException이라 명명하였습니다.
      */
      @ExceptionHandler(Exception.class)
      /*
      해당 Exception이 발생하면 Response에 출력되는 HttpStatus Code가 500으로 내려가도록 설정합니다.
      참고로 성공 시엔 HttpStatus code가 200으로 내려갑니다. 실습에서 HttpStatus Code의 역할은 성공이냐(200)
      아니냐 정도의 의미만 있고 실제 사용하는 성공 실패 여부는 json으로 출력되는 정보를 이용합니다.
      */
      @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
      protected CommonResult defaultException(HttpServletRequest request, Exception e) {
      return responseService.getFailResult(Integer.valueOf(getMessage("unKnown.code")), getMessage("unKnown.msg"));
      }
      @ExceptionHandler(CUserNotFoundException.class)
      @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
      protected CommonResult userNotFoundException(HttpServletRequest request, CUserNotFoundException e) {
      // 예외 처리의 메시지를 MessageSource에서 가져오도록 수정
      return responseService.getFailResult(Integer.valueOf(getMessage("userNotFound.code")), getMessage("userNotFound.msg"));
      }
      // 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());
      }
      }
    • findUserById API에서 파라미터에 lang값을 통해 국가명을 입력하게 설정합니다.
    • @ApiOperation(value = "회원 조회", notes = "userId로 회원을 조회한다")
      @GetMapping(value = "/users/{id}")
      @LoginCheck(type = LoginCheck.UserType.ADMIN)
      public SingleResult<UserDTO> findUserById(String accountId, @ApiParam(value = "회원ID", required = true) @PathVariable("id") String id,
      @ApiParam(value = "언어", defaultValue = "ko") @RequestParam String lang) {
      return responseService.getSingleResult(Optional.ofNullable(mapper.getUserProfile(id)).orElseThrow(CUserNotFoundException::new));
      }
    • 아래 내용은 Swagger API에 en설정을 통해 ResponseBody에
      msg값이 영어로 출력되는 걸 확인할 수 있습니다. 

 


  • [ 결과 ]
    • 중고거래 서비스가 해외 국가에서도 배포가 가능하게 예외 메시지를 다국어 처리를 하였습니다.
    • Spring에서 i18n세팅 후 MessageSource를 이용하여 국가에 해당하는 언어로 출력되도록 개 발하였습니다.
    • LocaleChangeInterceptor를 사용하여 lang이라는 RequestParameter가 요청에 있으면 해당 값을 로케일 정보로 변경하게 했습니다.
    • 로케일 정보는 기본으로 Session에서 읽어 오고 저장하도록 SessionLocaleResolver를 사용하였습니다.
    • Spring의 i18n 라이브러리에서는 각 종 리졸버를 제공함으로써 특정 상황에 맞게 활용할 수 있음을 알았습니다.
    • 결과적으로 다국어 환경에서도 요구사항에 맞게 개발이 가능해졌습니다.

  • [ 성과 ]
    • Spring i18n 다국어 지원을 통해 국가별 언어를 설정 방법을 학습하였습니다.
    • MessageConfig를 설정하면서 @Configuration을 통해 스프링 컨테이너가 bean을 정의하고 
      런타임 시 정의한 Bean들이 요청을 처리하는  spring의 IOC컨테이너의 개념과 생성원리를 학습하였습니다.
    • LocaleChangeInterceptor, SessionLocaleResolver를 이용해 세션정보에 지역정보를 설정하면서
      인터셉터, 리졸버의 동작원리에 대해 학습하였습니다. 

 


 

junshock5/used-market-server

중고거래 프로젝트. Contribute to junshock5/used-market-server development by creating an account on GitHub.

github.com

+ Recent posts