본문 바로가기

프로젝트

[spring] Spring Message Rest API와 사용

thymeleaf과 같은 뷰 템플릿을 이용할 때는 자동으로 코드를 대응되는 메시지로 변경해주지만, rest api 형식으로 구현하게 되면 이러한 편의성이 제공되지 않는다. Spring 입장에서는 사용자가 정말 응답에 국제화를 제공하고 싶은지 알 방법이 없기 때문에 당연한 것 같긴 하다.

Rest API에서 예외 메시지 등을 국제화하고 싶다면 필요한 위치에 MessageSource을 주입받아 사용한다. Spring boot을 이용하면 MessageSource는 기본적으로 MessageSourceAutoConfiguration 설정을 통해 Bean 등록 되어 있으므로 별도로 설정하지 않아도 된다.

사용한 설정은 다음과 같다.

spring:
  config:
    import: optional:secrets.yml
  messages:
    basename: messages,errors

 

일반 메시지와 에러 메시지를 구분하기 위해 spring.messages.basename에 폴더 이름을 지정해준다. 추가적으로 비밀 설정을 저장하기 위해 spring.config.import를 통해 secrets.yml 파일 안의 설정을 읽어올 수 있게 한다.

@RestControllerAdvice
public class GlobalExceptionHandler {
    @Autowired
    private MessageSource messageSource;

    @ExceptionHandler(MethodArgumentNotValidException.class) // 요청의 유효성 검사 실패 시
    @ResponseStatus(HttpStatus.BAD_REQUEST) // 400 Bad Request로 응답 반환
    public Map<String, String> handleInValidRequestException(MethodArgumentNotValidException e) {
        Locale locale = LocaleContextHolder.getLocale();
        // 에러가 발생한 객체 내 필드와 대응하는 에러 메시지를 map에 저장하여 반환
        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getFieldErrors().forEach(error -> {
            String fieldName = error.getField();

            String errorMessage = messageSource.getMessage(error, locale);
            errors.put(fieldName, errorMessage);
        });
        // global error 도 지원
        e.getBindingResult().getGlobalErrors().forEach(error -> {
            String objectName = error.getObjectName();

            String errorMessage = messageSource.getMessage(error, locale);
            errors.put(objectName, errorMessage);
        });

        return errors;
    }

    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String,String> handleInValidRequestException(MethodArgumentTypeMismatchException e) {
        String code = e.getErrorCode();
        String fieldName = e.getName();
        Locale locale = LocaleContextHolder.getLocale(); // 현재 스레드의 로케일 정보를 가져온다.
        String errorMessage = messageSource.getMessage(code, null, locale); // 국제화 된 메시지를 가져온다.

        return Map.of(fieldName, errorMessage);
    }
    
    @ExceptionHandler({BaseException.class})
    public ResponseEntity<ErrorResponse> handleAllBaseException(BaseException e) {
        var code = e.getErrorCode();
        var status = code.getHttpStatus();
        var message = code.getMessage();

        Locale locale = LocaleContextHolder.getLocale(); // 현재 스레드의 로케일 정보를 가져온다.
        String errorMessage = messageSource.getMessage(message, null, locale); // 국제화 된 메시지를 가져온다.

        return ResponseEntity
                .status(status)
                .body(ErrorResponse.from(code.name(), errorMessage));
    }
}

 

MessageSource은 locale 정보를 요구하는데, LocaleContextHolder.getLocale( ) 메서드를 이용하면 현재 스레드 - 즉 현재 유저의 locale 정보를 가져올 수 있으므로 이걸 이용하여 사용자의 국가에 맞는 예외 메시지를 반환하도록 만든다.

 위와 같이 ControllerAdvice에서 Exception을 잡아 메시징을 수행하면 보다 편리하게 예외 메시지를 처리할 수 있다. 예시의 프로젝트에서는 BaseException을 만들고, 서버에서 사용하는 예외에서 이를 상속하게 해 일관된 방식으로 메시징을 처리할 수 있었다.