새로운 에러 응답 표준, ProblemDetail

송영호·2025년 8월 8일

Spring Boot

목록 보기
7/8

개요

최근 새 프로젝트에서 에러 처리 방식을 통합하는 일을 맡았다.
그동안은 예외가 발생하면 서비스마다 직접 만든 ErrorResponse DTO를 만들어서 반환하는 방식이었다.
문제는 이 구조가 팀마다 제각각이라 API 문서 표준화가 힘들고, 공통 에러 코드나 필드 체계가 맞지 않는 경우가 잦았다는 점이다.

“혹시 표준화된 에러 응답 방식이 있지 않을까?” 하고 찾아보다가 Spring Framework 6에서 새롭게 제공하는 ProblemDetail 클래스를 알게 됐다.
이 클래스는 HTTP API 에러 응답을 표준화하는 국제 규격인 RFC 7807을 그대로 구현한 것이다.

이번 글에서는 기존 커스텀 DTO 방식과의 차이를 살펴보고, ProblemDetail을 활용해 에러를 처리하는 방법을 정리해본다.

기존 방식

기존에는 예외 발생 시 직접 만든 DTO를 반환하는 패턴

// error 처리용 DTO 정의
public record ErrorResponse(int status, String message, String code) {}

// 예외를 잡아서 ErrorResponse로 변환
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handle(CustomException ex) {
    return ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .body(new ErrorResponse(400, ex.getMessage(), "BAD_REQUEST"));
}

이 방식은 단순하지만 단점이 많다.

  • 서비스마다 필드 구조가 달라 API 문서 표준화가 어렵다
  • 국제 표준(RFC 7807) 같은 공통 규격을 따르지 않는다
  • 다른 시스템과 연동할 때, 에러 응답 포맷이 맞지 않아 변환 로직이 추가로 필요하다

ProblemDetail

개념

ProblemDetail은 RFC 7807 – Problem Details for HTTP APIs를 구현한 클래스다.
이 RFC는 REST API에서 에러가 발생했을 때 일관된 JSON 응답 구조를 제공하기 위한 규격이다.
Spring 6 이상에서 기본으로 제공하며, 응답은 application/problem+json 형태로 내려간다.

기본 필드

필드설명
type오류 유형을 식별하는 URI
title오류 제목
statusHTTP 상태 코드
detail오류에 대한 상세 설명
instance오류가 발생한 리소스 경로

적용

@ControllerAdvice
@Slf4j
public class ExceptionControllerAdvice extends ResponseEntityExceptionHandler {

    // 도메인 예외(CustomException) 처리
    @ExceptionHandler(CustomException.class)
    public ProblemDetail handleCustomException(CustomException e) {
        return createProblemDetail(e.getErrors(), e, e.getErrors().getMessage());
    }

    // ProblemDetail 생성 메서드
    private ProblemDetail createProblemDetail(Errors error, Exception e, String message) {
        // 상태 코드와 detail 설정
        ProblemDetail detail = ProblemDetail.forStatusAndDetail(error.getHttpStatus(), e.getMessage());

        // 표준 필드 외에 커스텀 정보 추가
        detail.setProperty("errorCode", error.getErrorCode());
        detail.setProperty("errorMessage", message);
        detail.setProperty("timestamp", LocalDateTime.now());
        detail.setProperty("exception", e.getClass().getSimpleName());

        log.error("Exception caught: {}", e.toString());

        return detail;
    }
}

코드 설명

@ControllerAdvice

  • 전역적으로 예외를 처리할 수 있는 어노테이션.
  • 컨트롤러마다 중복된 예외 처리 코드를 작성할 필요가 없다.

ResponseEntityExceptionHandler

  • 상속 받으면, 스프링에서 기본적으로 발생하는 예외(MethodArgumentNotValidException, HttpMessageNotReadableException 등)를 미리 처리할 수 있게 해준다.

@ExceptionHandler(CustomException.class)

  • 직접 정의한 도메인 예외를 잡아서 처리한다.
  • 반환 타입을 ProblemDetail로 지정하면, 스프링이 자동으로 application/problem+json 응답을 만들어준다.

ProblemDetail.forStatusAndDetail(...)

  • HTTP 상태 코드와 오류 설명(detail)을 한 번에 설정한다.

setProperty(...)

  • 표준 필드 외에 추가 정보(예: 에러 코드, 발생 시간, 예외 클래스 이름)를 JSON 응답에 포함시킨다.

응답 예시

{
    "type": "about:blank",
    "title": "Bad Request",
    "status": 400,
    "instance": "/api/mini-room-component",
    "errorCode": "E004",
    "errorMessage": "이미 배치된 아이템이에요.",
    "timestamp": "2025-08-08 15:56:25",
    "exception": "DuplicateComponentException"
}

기존 방식과 비교

구분기존 방식 (ErrorResponse DTO)ProblemDetail
표준 여부팀·서비스마다 구조 제각각, 표준 없음RFC 7807 기반 국제 표준
필드 구성status, message, code 등 개발자가 임의 정의type, title, status, detail, instance + 커스텀 속성
호환성내부 서비스 중심 설계, 외부 연동 시 변환 필요표준 규격 준수, 외부 API/클라이언트와 호환 용이
확장성필드 변경 시 클라이언트 파싱 로직 수정 필요setProperty()로 확장 가능, 표준 필드는 유지
문서화Swagger 등에서 별도 스키마 작성 필요표준 필드 기반으로 문서화 용이
유지보수성서비스별 DTO 관리 필요, 중복 코드 발생Spring 기본 지원, DTO 생성 불필요
도입 난이도간단하지만 규칙 합의 필요Spring 6 이상 필요

마무리

ProblemDetail은 기존처럼 매번 에러 DTO를 만드는 수고를 줄여주고, 국제 표준을 기반으로 API 에러 응답을 통일할 수 있게 해준다.
이 방식으로 전환하면 API 문서화가 쉬워지고, 다른 시스템과의 연동에서도 호환성이 높아진다.

새로운 프로젝트라면 처음부터 ProblemDetail을 사용하는 걸 추천한다.
기존 프로젝트라면 전환 시점에 클라이언트 파싱 로직도 함께 맞춰주는 것이 좋다.

profile
BACKEND 개발자

0개의 댓글