SpringBoot

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

pepega 2021. 10. 22. 14:06

시작 전 변경 사항이 있다! (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 Groovy DSL I found No libraries found for 'dev.akkinoc.util.YamlResourceBundle' To Reproduce implementation 'dev.akkinoc.util:yaml-resource-bundle:2.4.1'...

github.com

 

해당 글에서

implementation 'net.rakugakibox.util:yaml-resource-bundle:1.1'

를 사용하지만

버전 업이 되었고

코틀린만 진행하는 것 같다.

 

아쉽지만

springframework 기반의 properties를 사용하기로 했다.

 

종속성이 사라져서 좋긴 하다.

 

 

수정된 코드는

https://github.com/GHGHGHKO/goose-auth-api-server/commit/ba3be18b623fc2d6bf951d8d505e9402a5447c22

 

feat: i18n 추가(ko_KR, en) · GHGHGHKO/goose-auth-api-server@ba3be18

Show file tree Hide file tree Showing 6 changed files with 63 additions and 9 deletions.

github.com

 

 

 

전체 소스코드

https://github.com/GHGHGHKO/Springboot/tree/main/pepega_chapter_7

 

GitHub - GHGHGHKO/Springboot: 블로그에 업로드 된 소스코드

블로그에 업로드 된 소스코드. Contribute to GHGHGHKO/Springboot development by creating an account on GitHub.

github.com

 

 

이전 포스팅에서는

최상위 Exception 대신 Custom Exception 클래스를 만들어

변경 시에도 예외 처리가 동일하게 처리되는 것을 확인했다.

이번 포스팅에서는

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의 장점을 살려 메시지 파일을 저장하려고 한다.

그렇기 위해서는 아래의 라이브러리를 추가하여야 한다.

https://github.com/akkinoc/yaml-resource-bundle

 

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

buid.gradle 파일의 dependencies에 다음을 추가한다.

implementation 'net.rakugakibox.util:yaml-resource-bundle:1.1'

 

MessageConfiguration 파일 생성

com.example.pepega.config 하위에 MessageConfiguration을 생성한다.

Spring에서 제공하는 LocaleChangeInterceptor를 사용하여 lang이라는 RequestParameter가

요청에 있으면 해당 값을 읽어 locale 정보를 변경한다.

아래에서 locale 정보는 기본으로 Session에서 읽어오고

저장하도록 SessionLocaleResolver를 사용하였는데 아래와 같이 다른 Resolver도 있으므로

상황에 따라 적절한 Resolver를 설정하여 사용하면 된다.

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/i18n/package-summary.html

 

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

Package org.springframework.web.servlet.i18n Description Locale support classes for Spring's web MVC framework. Provides standard LocaleResolver implementations, and a HandlerInterceptor for locale changes.

docs.spring.io

 

package com.example.pepega.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);
        }
    }
}

 

application.yml에 i8n 경로 및 인코딩 정보 추가

server:
  port: 8080

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/test
    driver-class-name: org.h2.Driver
    username: sa
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    properties.hibernate.hbm2ddl.auto: none
    showSql: true
  messages:
    basename: i18n/exception
    encoding: UTF-8

다국어 처리 message yml 파일 작성

resources 아래에 i18n 디렉터리를 생성하고

exception_en.yml, exception_ko.yml 을 생성한다. 하단과 같이 입력한다.

메시지 값은 모두 String으로 정의해야 한다.

# exception_ko.yml
unKnown:
  code: "-9999"
  msg: "알 수 없는 오류가 발생하였습니다. SadPepe :("
userNotFound:
  code: "-1000"
  msg: "존재하지 않는 회원입니다. SadPepe :("
# exception_en.yml
unKnown:
  code: "-9999"
  msg: "An unknown error has occurred. SadPepe :("
userNotFound:
  code: "-1000"
  msg: "This member not exist. SadPepe :("

 

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

public CommonResult getFailResult(int code, String msg) {
        CommonResult result = new CommonResult();
        result.setSuccess(false); // setSuccess : 응답 성공 여부 (true/false)
        result.setCode(code); // setCode : 응답 코드 번호 >= 0 정상, < 0 비정상
        result.setMsg(msg); // setMsg 응답 메시지
        return result;
    }

 

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

package com.example.pepega.advice;

import com.example.pepega.advice.exception.CUserNotFoundException;
import com.example.pepega.model.response.CommonResult;
import com.example.pepega.service.ResponseService;
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 javax.servlet.http.HttpServletRequest;

@RequiredArgsConstructor
@RestControllerAdvice // 예외 발생 시 json 형태로 결과 반환 | 프로젝트의 모든 Controller에 로직 적용
// @RestControllerAdvice(basePackages = "com.example.pepega") : pepega 하위의 Controller 에만 로직 적용
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) {
        // CommonResult : 응답 결과에 대한 정보
        return responseService.getFailResult(Integer.valueOf(getMessage("unKnown.code")), getMessage("unKnown.msg"));
        // 예외 처리 메시지를 MessageSource에서 가져오도록 수정, exception_ko.yml 파일에서 가져온 것임
        // getFailResult : setSuccess, setCode, setMsg
    }

    @ExceptionHandler(CUserNotFoundException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    protected CommonResult userNotFoundException(HttpServletRequest request, CUserNotFoundException e) {
        // CommonResult : 응답 결과에 대한 정보
        return responseService.getFailResult(Integer.valueOf(getMessage("userNotFound.code")), getMessage("userNotFound.msg"));
        // 예외 처리 메시지를 MessageSource에서 가져오도록 수정
        // getFailResult : setSuccess, setCode, setMsg
    }

    // 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());
    }
}

이제 서버를 실행해보겠다.

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

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

다시 서버를 시작한다.

예외 메시지 처리를 MessageSource를 이용해 개선해 봤다.

부가적으로는 다국어 처리도 가능해져 api를 폭 넓게 사용할 수 있게 되었다.

출처

https://daddyprogrammer.org/post/499/springboot2-exception-handling-with-messagesource/

 

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

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

daddyprogrammer.org

https://github.com/codej99/SpringRestApi

 

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