본문으로 건너뛰기
개발 머꼬
개발 노트Spring
hohyeon.dev28

예외마다 응답 형식이 달라지던 것을 ProblemDetail로 통일한 과정

  • #API 설계
  • #Engineering Note
  • #Spring

문제 발생

예외 처리기를 필요할 때마다 추가하다 보니 오류 응답이 제각각이었습니다.

{ "message": "not found" }
{ "error": "FORBIDDEN", "code": 403 }
{ "timestamp": "...", "status": 500, "path": "/api/posts" }

클라이언트는 세 가지 파서를 유지해야 했고, Spring 내장 예외(검증 실패, 잘못된 미디어 타입)는 또 다른 형식으로 나갔습니다.

원인 분석

표준 형식이 이미 프레임워크에 들어 있습니다. Spring 문서는 RFC 9457 "Problem Details for HTTP APIs"를 지원한다고 밝히며 네 가지 추상화를 소개합니다.

  • ProblemDetail — 명세가 정의한 표준 필드와 커스텀 비표준 필드를 담는 컨테이너
  • ErrorResponse — HTTP 상태, 헤더, RFC 9457 형식의 본문을 노출하는 계약. 모든 Spring MVC 예외가 이 인터페이스를 구현합니다
  • ErrorResponseException — 커스텀 예외의 기반 클래스로 쓰기 좋은 기본 구현
  • ResponseEntityExceptionHandler@ControllerAdvice의 기반 클래스로, 모든 Spring MVC 예외와 ErrorResponseException을 처리해 RFC 9457 형식 본문으로 렌더합니다

처리 규칙도 정해져 있습니다 — ProblemDetailstatus가 HTTP 상태를 결정하고, instance가 비어 있으면 현재 URL 경로로 채워지며, Jackson 코덱이 application/problem+json을 producible 미디어 타입으로 사용합니다.

해결 방안

  1. 전역 핸들러를 기반 클래스에서 상속합니다. 내장 예외까지 한 번에 형식이 통일됩니다.
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

  @ExceptionHandler(PostNotFoundException.class)
  ProblemDetail handleNotFound(PostNotFoundException ex) {
    ProblemDetail body = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
    body.setType(URI.create("https://meokko.com/problems/post-not-found"));
    body.setTitle("게시글을 찾을 수 없습니다");
    return body;
  }
}
  1. Spring Boot에서는 설정 한 줄로 내장 예외를 켭니다.
spring.mvc.problemdetails.enabled=true

문서 설명대로 이 설정은 내장 예외를 problem details로 처리하는 핸들러를 order 0으로 자동 구성하므로, 우리 @ControllerAdvice는 그보다 앞선 order를 갖게 둡니다.

  1. 기계가 읽을 값은 확장 필드에 넣습니다. getProperties() 맵에 넣으면 Jackson이 최상위 JSON 속성으로 펼쳐줍니다 — 사람이 읽는 detail을 클라이언트가 파싱하게 만들지 않습니다.

  2. 커스텀 예외는 ErrorResponseException을 상속합니다. 예외 자체가 상태와 본문을 알고 있으면 핸들러가 얇아집니다.

  3. 메시지 국제화도 여기에 붙습니다. 문서가 설명하는 메시지 코드 전략(problemDetail.title.<예외 클래스>)을 MessageSource로 풀면 사용자 언어에 맞는 문구가 나갑니다.

  4. 스택 트레이스를 담지 않습니다. RFC 9457의 보안 고려사항이자 이 저장소의 원칙이기도 합니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.