티스토리 뷰

스프링 부트 REST API WEB 프로젝트

깃헙 링크

https://github.com/choiwoonsik/springboot_RestApi_App_Project/tree/main/restApiSpringBootApp

수행 목록

  1. 환경구성 및 helloworld 출력
  2. H2 DB 연동
  3. Swagger API 문서 연동
  4. REST API 설계
  5. RestControllerAdvice를 이용한 통합 예외 처리
  6. Entity - DTO 분리
  7. MessageSource를 이용해 예외 메시지 다국화
  8. JPA Aduting을 이용해 객체 생성시간/수정시간 적용
  9. 스프링 시큐리티 + Jwt를 이용해서 인증 및 권한 체크
  10. 스프링 시큐리티 AuthenticationEntryPoint, AccessDenied로 인증 및 인가 예외처리
  11. Jwt AccessToken + RefreshToken으로 보안성과 사용자 편의성 고도화하기
  12. JUnit Test (단위 테스트)
  13. JUnit Test (통합 테스트)
  14. OAuth 2.0 정리
  15. OAuth 2.0 카카오 로그인 part.1 Authorization code + Token 발급
  16. OAuth 2.0 카카오 로그인 part.2 토큰으로 회원 가입 / 로그인
  17. OAuth 2.0 카카오 로그인 테스트 검증
  18. 환경별 설정을 위해서profile 분리하기 

앞서 ExceptionHandler, RestControllerAdvice를 이용해서 예외발생시 한곳에서 처리하도록 만들었다. 발생하는 에러에 맞게 구분하였지만 출력하고 있는 에러메시지는 단 두가지로 

  • SUCCESS(0, "성공하였습니다."),
  • FAIL(-1, "실패하였습니다.");

만을 제공하고 있다. 이러한 에러메시지를 더 진보시켜보자.

  1. 발생하는 예외에 맞게 예외 메시지 출력
  2. 원하는 언어에 맞게 다국어로 제공

스프링에서는 언어 요청에 맞게 다국어 처리를 하기위해 i18n세팅을 지원한다. i18n은 Internationalization의 약자로 i와n사이에 18글자가 있다는 것을 의미한다. (k8s와 같은 축약 방법)


1. application.yml에서 i18n설정을 위한 의존성 추가

위 링크를 참고해서 yml에서 다국화를 하기 위해 build.gradle에 의존성을 추가하자.

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

2. MessageConfiguratoin 클래스 생성

@Configuration
public class MessageConfiguration implements WebMvcConfigurer {

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

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
        lci.setParamName("lang");
        return lci;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Bean
    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;
    }

    private static class YamlMessageSource extends ResourceBundleMessageSource {
        @Override
        protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
            return ResourceBundle.getBundle(basename, locale, YamlResourceBundle.Control.INSTANCE);
        }
    }
}

 

LocaleChangeInterceptor의 역활
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

LocaleChangeInterceptor는 spring에서 공식적으로 국제화 처리를 위해 제공하는 인터셉터이다. 이 인터셉터를 이용해서 "lang"이름을 갖는 쿼리파라미터의 값을 바탕으로 언어정보를 변경한다.

 

LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
        lci.setParamName("lang");

 

  • 지역설정을 변경하는 인터셉터를 만든다.
  • 요청시 쿼리 파라미터로 lang정보를 넣으면 변경된다 (lang="en")
registry.addInterceptor(localeChangeInterceptor());
  • 이 인터셉터를 시스템 레지스트리에 추가해준다.
  • 이 추가를 위해서 WebMvcConfigurer를 상속받는 것이다.

 

LocaleResolver & SessionLocaleResolver의 역활
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

LocaleResolver는 다양한 방식의 Resolver를 제공하는데 그중에서 SessionLocaleResolver를 사용한다.

 

LocaleChangeInterceptor로 "lang"값을 통해 Locale 정보가 변경되면 SessionLocaleResolver에 의해서 해당 세션의 Locale 정보가 변경되는 것이다. 그러면 해당 세션이 만료될때까지 변경된 Locale정보가 유지될것이다.

 

slr.setDefaultLocale(Locale.KOREAN);
  • 세션에 지역설정을 한다. 위 설정을 통해 "lang"쿼리의 값에 매칭되는 yml파일이 없다면 기본으로 한국어를 제공하게 된다.

 

MessageSource의 역활

yml파일을 참조하는 MessageSource를 생성했다. basename과 encoding값을 application.yml에서 받아오는 애노테이션이 달려있다.

@Value("${spring.messages.basename}") String basename,
@Value("${spring.messages.encoding}") String encoding

yml에 있는 설정정보를 가져와서 세팅된다.

spring:
  messages:
    basename: i18n/exception
    encoding: UTF-8

 

여기서 basename이 i18n/exception으로 설정되어있는데 이는 "resources/i18n/" path에서 "exception_로케일.yml" 파일을 읽어서 해당 내용으로 표시하라는 뜻이다. 인코딩은 UTF-8로 하겠다는 것.

 

  • setAlwaysUseMessageFormat : MessasgeFormat을 전체 메시지에 적용할 것인지 여부 (T/F)
  • setFallbackToSystemLocale : 감지된 Locale파일이 없을 때
    • (T) -> system locale값 사용
    • (F) -> messages.properties값 사용
  • setUseCodeAsDefaultMessage : 메시지를 찾지 못했을 때, 예외 처리 대신 메시지 코드를 그대로 반환할지 말지 (T/F)

 

i18n/exception_en.yml
unKnown:
  code: "-9999"
  msg: "An unknown error has occurred."
userNotFound:
  code: "-1000"
  msg: "This member not exist"

 

YamlMessageSource
protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
    return ResourceBundle.getBundle(basename, locale, YamlResourceBundle.Control.INSTANCE);
}

locale정보에 따라 맞는 yml파일을 basename과 locale 조합으로 찾아서 읽는다.

 

3. ExceptionAdvice를  코드와 message를 받도록 수정

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

    private String getMessage(String code) {
        return getMessage(code, null);
    }

    private String getMessage(String code, Object[] args) {
        return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
    }
    // 실패 결과만 처리
    public CommonResult getFailResult(int code, String msg) {
        CommonResult result = new CommonResult();
        result.setSuccess(false);
        setFailResult(result, code, msg);
        return result;
    }
    
    // API 요청 실패 시 응답 모델을 실패 데이터로 세팅
    private void setFailResult(CommonResult result, int code, String msg) {
        result.setSuccess(false);
        result.setCode(code);
        result.setMsg(msg);
    }

 

4. UserController에서 lang 쿼리 파라미터를 받을 수 있게 변경

    @ApiOperation(value = "회원 단건 검색", notes = "userId로 회원을 조회합니다.")
    @GetMapping("/user/id/{userId}")
    public SingleResult<UserRequestDto> findUserById
            (@ApiParam(value = "회원 ID", required = true) @PathVariable Long userId,
             @ApiParam(value = "언어", defaultValue = "ko") @RequestParam String lang) {
        return responseService.getSingleResult(userService.findById(userId));
    }

결과

 

좀 많이 복잡한 내용으로 새로운것들이 너무 많아서 어려웠다. yml파일을 읽어서 코드와 메시지를 읽도록 하는 방법같은 경우도 처음 보고, 인터셉터 내용도 처음 접해봐서 어려움이 많았다. 관련 내용을 더 찾아보고 적용해봐야겠다.

 

반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday