[Spring] Problem Details (RFC 7807) MessageSource 한국어 패치

0
  1. application.yaml 설정

    /src/main/resources/application.yaml

spring:
  mvc:
    problemdetails:
      enabled: true
  messages:
    basename: messages/errors
  web:
    resources:
      add-mappings: false
  • problemdetails.enabled: true
    Spring MVC가 ProblemDetail을 활용할 것을 지정

  • messages.basename: messages/errors
    메시지소스(properties) 파일의 경로와 이름을 지정

  • web.resources.add-mappings: false
    Spring가 자동처리하는 정적 자원 파일 mapping을 비활성화

/src/main/resources/messages/errors.properties

# AsyncRequestTimeoutException
problemDetail.title.org.springframework.web.context.request.async.AsyncRequestTimeoutException=요청 시간 초과
problemDetail.org.springframework.web.context.request.async.AsyncRequestTimeoutException=요청 처리 시간이 제한 시간을 초과했습니다.
# ConversionNotSupportedException
problemDetail.title.org.springframework.beans.ConversionNotSupportedException=형식 변환 실패
problemDetail.org.springframework.beans.ConversionNotSupportedException=프로퍼티 {0}의 값을 {1} 형식으로 변환할 수 없습니다.
# HandlerMethodValidationException
problemDetail.title.org.springframework.web.bind.support.HandlerMethodValidationException=데이터 검증 실패
problemDetail.org.springframework.web.bind.support.HandlerMethodValidationException=데이터 검증 중 오류 발생: {0}
# HttpMediaTypeNotAcceptableException
problemDetail.title.org.springframework.web.HttpMediaTypeNotAcceptableException=지원되지 않는 응답 타입
problemDetail.org.springframework.web.HttpMediaTypeNotAcceptableException=지원 가능한 미디어 타입: {0}
problemDetail.org.springframework.web.HttpMediaTypeNotAcceptableException.parseError=미디어 타입 파싱 중 오류가 발생했습니다.
# HttpMediaTypeNotSupportedException
problemDetail.title.org.springframework.web.HttpMediaTypeNotSupportedException=지원되지 않는 요청 타입
problemDetail.org.springframework.web.HttpMediaTypeNotSupportedException=미디어 타입 {0}은(는) 지원되지 않습니다. 지원 타입: {1}
problemDetail.org.springframework.web.HttpMediaTypeNotSupportedException.parseError=미디어 타입 파싱 중 오류가 발생했습니다.
# HttpMessageNotReadableException
problemDetail.title.org.springframework.http.converter.HttpMessageNotReadableException=요청 본문 처리 오류
problemDetail.org.springframework.http.converter.HttpMessageNotReadableException=요청 본문을 읽을 수 없습니다. 데이터 형식을 확인하세요.
# HttpMessageNotWritableException
problemDetail.title.org.springframework.http.converter.HttpMessageNotWritableException=응답 생성 오류
problemDetail.org.springframework.http.converter.HttpMessageNotWritableException=응답 본문 생성 중 오류가 발생했습니다.
# HttpRequestMethodNotSupportedException
problemDetail.title.org.springframework.web.HttpRequestMethodNotSupportedException=지원되지 않는 HTTP 메서드
problemDetail.org.springframework.web.HttpRequestMethodNotSupportedException=HTTP 메서드 {0}은(는) 지원되지 않습니다. 사용 가능 메서드: {1}
# MethodArgumentNotValidException
problemDetail.title.org.springframework.web.bind.MethodArgumentNotValidException=유효성 검증 실패
problemDetail.org.springframework.web.bind.MethodArgumentNotValidException=요청 데이터가 유효하지 않습니다.
# MissingRequestHeaderException
problemDetail.title.org.springframework.web.bind.MissingRequestHeaderException=헤더 누락
problemDetail.org.springframework.web.bind.MissingRequestHeaderException=필수 헤더 {0}이(가) 누락되었습니다.
# MissingServletRequestParameterException
problemDetail.title.org.springframework.web.bind.MissingServletRequestParameterException=파라미터 누락
problemDetail.org.springframework.web.bind.MissingServletRequestParameterException=필수 파라미터 {0}이(가) 누락되었습니다.
# MissingMatrixVariableException
problemDetail.title.org.springframework.web.bind.MissingMatrixVariableException=매트릭스 변수 누락
problemDetail.org.springframework.web.bind.MissingMatrixVariableException=필수 매트릭스 변수 {0}이(가) 누락되었습니다.
# MissingPathVariableException
problemDetail.title.org.springframework.web.bind.MissingPathVariableException=경로 변수 누락
problemDetail.org.springframework.web.bind.MissingPathVariableException=경로 변수 {0}을(를) 찾을 수 없습니다.
# MissingRequestCookieException
problemDetail.title.org.springframework.web.bind.MissingRequestCookieException=쿠키 누락
problemDetail.org.springframework.web.bind.MissingRequestCookieException=필수 쿠키 {0}이(가) 누락되었습니다.
# MissingServletRequestPartException
problemDetail.title.org.springframework.web.multipart.support.MissingServletRequestPartException=파일 데이터 누락
problemDetail.org.springframework.web.multipart.support.MissingServletRequestPartException=필수 파일 또는 폼 데이터 {0}이(가) 누락되었습니다.
# NoHandlerFoundException
problemDetail.title.org.springframework.web.servlet.NoHandlerFoundException=잘못된 접근
problemDetail.org.springframework.web.servlet.NoHandlerFoundException=요청한 API 엔드포인트가 존재하지 않습니다.
# NoResourceFoundException
problemDetail.title.org.springframework.web.NoResourceFoundException=리소스 없음
problemDetail.org.springframework.web.NoResourceFoundException=요청한 리소스를 찾을 수 없습니다.
# TypeMismatchException
problemDetail.title.org.springframework.beans.TypeMismatchException=타입 불일치
problemDetail.org.springframework.beans.TypeMismatchException=프로퍼티 {0}의 값 {1}이(가) 요구되는 타입과 일치하지 않습니다.
# UnsatisfiedServletRequestParameterException
problemDetail.title.org.springframework.web.bind.UnsatisfiedServletRequestParameterException=파라미터 조건 미충족
problemDetail.org.springframework.web.bind.UnsatisfiedServletRequestParameterException=다음 조건을 충족하지 않았습니다: {0}
@RestControllerAdvice
class ResponseEntityExceptionHandlerAdapter : ResponseEntityExceptionHandler() {
    override fun handleMethodArgumentNotValid(
        ex: MethodArgumentNotValidException,
        headers: HttpHeaders,
        status: HttpStatusCode,
        request: WebRequest,
    ): ResponseEntity<in Any>? {
        val body: ProblemDetail = ex.updateAndGetBody(messageSource, LocaleContextHolder.getLocale())
        val fieldErrors =
            ex.fieldErrors.map { fieldError ->
                mapOf(
                    "field" to fieldError.field,
                    "value" to fieldError.rejectedValue,
                    "message" to fieldError.defaultMessage,
                )
            }

        body.setProperty("errors", fieldErrors)
        return handleExceptionInternal(
            ex,
            body,
            headers,
            status,
            request,
        )
    }
}

이렇게 구성하면 Spring MVC 예외 처리 구조에서 ProblemDetail로 각종 예외를 형식화하고 한국어로 출력할 수 있다.

{
    "type": "about:blank",
    "title": "유효성 검증 실패",
    "status": 400,
    "detail": "요청 데이터가 유효하지 않습니다.",
    "instance": "/",
    "errors": [
        {
            "field": "reason",
            "value": null,
            "message": "사유는 비어있을 수 없습니다."
        }
    ]
}

고민 해볼만한 점

  • 게이트웨이를 통한 라우팅
    - API Gateway를 사용하면 요청이 origin endpoint로 바로 가지 않고, 중간 게이트웨이를 거쳐서 라우팅된다.
    - 이때 instance 정보(ex: 어떤 서버에서 처리했는지 등)는 원래 의도한 정보와 다르게 의미가 약해진다.
    - 게이트웨이 레벨에서 Load Balancer나 리라이트/포워딩 전략이 다를 수 있기 때문.
    - 실제 요청 처리 인스턴스가 불명확하거나, instance 정보 자체가 왜곡될 수 있다.

0개의 댓글