다플 생활 가이드

정책·신청 안내

Spring Boot REST API 예외 처리 ExceptionHandler 완벽 정리

Spring Boot를 활용하여 REST API를 개발할 때, 웹 애플리케이션의 완성도를 결정짓는 중요한 요소 중 하나가 바로 예외 처리(Exception Handling)입니다. 클라이언트 시스템이나 모바일 애플리케이션과 통신하는 환경에서는 예외가 발생했을 때 500 내부 서버 에러(Internal Server Error)나 가독성 낮은 에러 스택 트레이스를 그대로 반환하면 안 됩니다. 시스템의 안정성과 보안을 유지하고 프론트엔드 개발자와의 원활한 협업을 위해서는 정제된 구조의 에러 응답을 일관되게 제공해야 합니다. 이 글에서는 Spring Boot 환경에서 전역적으로 예외를 감지하고 제어할 수 있는 표준 아키텍처 구현 방식을 차근차근 살펴보겠습니다.

01. REST API 예외 처리 구조의 필요성

서버에서 예외가 발생했을 때 일관된 형태의 JSON 응답을 반환해야 하는 이유를 이해하는 것이 우선입니다.

✓ 클라이언트와의 약속

웹 애플리케이션 프론트엔드나 외부 오픈 API를 사용하는 클라이언트 관점에서는 API 호출이 성공했을 때뿐만 아니라 실패했을 때도 정형화된 데이터를 받아야만 프론트엔드 화면에서 에러 메시지를 유연하게 노출할 수 있습니다. 예를 들어 어떤 API는 에러가 났을 때 단순 텍스트를 반환하고, 다른 API는 HTML 에러 페이지를 반환한다면 클라이언트 측의 예외 처리 로직은 매우 복잡해질 수밖에 없습니다.

✓ 보안과 로깅의 분리

디버깅에 필요한 상세한 시스템 에러 로그(Stack Trace)는 서버 내부 로그 시스템에만 기록되어야 합니다. 이것이 외부에 노출되면 데이터베이스 구조나 사용 중인 라이브러리 버전 등 심각한 보안 취약점이 노출될 위험이 있습니다. 전역 예외 처리기를 구축하면 내부 로깅과 외부 응답 메시지를 깔끔하게 분리할 수 있습니다.

02. 핵심 컴포넌트인 ControllerAdvice와 ExceptionHandler

Spring 프레임워크는 컨트롤러 계층에서 발생하는 예외를 한곳으로 모아 처리할 수 있는 강력한 어노테이션들을 제공합니다.

✓ @RestControllerAdvice의 역할

@RestControllerAdvice는 기존 @ControllerAdvice 기능에 @ResponseBody가 더해진 어노테이션입니다. 개발자가 지정한 범위의 컨트롤러들에서 예외가 발생하면 이를 가로채어 모니터링하고, 최종적으로 JSON이나 XML 같은 객체 기반의 응답 본문(Response Body)을 직접 반환할 수 있도록 도와주는 컴포넌트입니다.

✓ @ExceptionHandler의 역할

@RestControllerAdvice로 선언된 클래스 내부의 메서드에 @ExceptionHandler를 부착하여 구체적으로 어떤 예외 클래스를 처리할지 지정합니다. 예를 들어 @ExceptionHandler(IllegalArgumentException.class) 유형으로 지정해 두면, 시스템 내 어디선가 인자 값이 잘못되어 해당 예외가 던져졌을 때 해당 메서드가 자동으로 호출되어 바인딩됩니다.

03. 공통 에러 응답 객체 및 에러 코드 설계

체계적인 예외 처리를 위해 가장 먼저 해야 할 일은 모든 에러 응답이 공유할 공통 포맷 객체와 도메인별 에러 코드를 정의하는 것입니다.

✓ 에러 코드 정의 (Enum)

단순한 HTTP 상태 코드 외에도, 비즈니스 로직상 구체적으로 어떤 오류가 발생했는지 알려주는 커스텀 에러 코드를 관리하면 좋습니다.

Java
public enum ErrorCode {
    INVALID_INPUT_VALUE(400, "C001", "잘못된 입력값입니다."),
    METHOD_NOT_ALLOWED(405, "C002", "허용되지 않은 메서드입니다."),
    ENTITY_NOT_FOUND(400, "C003", "대상을 찾을 수 없습니다."),
    INTERNAL_SERVER_ERROR(500, "C004", "서버 내부 오류가 발생했습니다.");

    private final int status;
    private final String code;
    private final String message;

    ErrorCode(int status, String code, String message) {
        this.status = status;
        this.code = code;
        this.message = message;
    }

    public int getStatus() { return status; }
    public String getCode() { return code; }
    public String getMessage() { return message; }
}

✓ 공통 에러 응답 객체 (ErrorResponse)

클라이언트에게 최종적으로 전달될 구조화된 JSON 데이터 포맷입니다.

Java
public class ErrorResponse {
    private String message;
    private String code;
    private int status;

    public ErrorResponse(ErrorCode errorCode) {
        this.message = errorCode.getMessage();
        this.code = errorCode.getCode();
        this.status = errorCode.getStatus();
    }

    public static ErrorResponse of(ErrorCode errorCode) {
        return new ErrorResponse(errorCode);
    }

    public String getMessage() { return message; }
    public String getCode() { return code; }
    public int getStatus() { return status; }
}

04. 전역 예외 처리기 구현 실전 예제

이제 앞서 만든 에러 코드와 응답 객체를 조립하여 실제 예외를 감지하고 처리하는 전역 핸들러 클래스를 작성해 보겠습니다.

✓ GlobalExceptionHandler 클래스 작성

이 핸들러 클래스는 프로젝트 내 애플리케이션 전역에서 발생하는 예외를 감지하여 일정한 규격으로 감싸 응답을 전달해 줍니다.

Java
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.HttpRequestMethodNotSupportedException;

@RestControllerAdvice
public class GlobalExceptionHandler {

    // 지원하지 않는 HTTP 메서드를 호출했을 때 발생하는 예외 처리
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    protected ResponseEntity<ErrorResponse> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        ErrorResponse response = ErrorResponse.of(ErrorCode.METHOD_NOT_ALLOWED);
        return new ResponseEntity<>(response, HttpStatus.METHOD_NOT_ALLOWED);
    }

    // 비즈니스 로직 및 런타임 오류 등 전반적인 예외 처리
    @ExceptionHandler(IllegalArgumentException.class)
    protected ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException e) {
        ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE);
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    // 최상위 Exception 시스템 오류 처리
    @ExceptionHandler(Exception.class)
    protected ResponseEntity<ErrorResponse> handleException(Exception e) {
        ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

이렇게 구성해 두면 컨트롤러 내부에서 무겁게 try-catch 구문을 일일이 작성할 필요가 없어지므로 복잡했던 비즈니스 로직 코드가 몰라보게 간결하고 깔끔해집니다.

05. 주요 예외 처리 컴포넌트 비교

Spring Web MVC 환경에서 혼동하기 쉬운 예외 처리 방식들의 특성과 범위를 아래 표로 비교해 두었습니다. 상황에 맞는 적절한 솔루션을 선택하시는 데 유용한 기준이 됩니다.

구분처리 범위주요 특징 및 한계점
Try-Catch 구문메서드 단위 로직 내부가장 세밀하게 예외를 제어할 수 있지만 코드가 중복되고 가독성이 낮아집니다.
@ExceptionHandler개별 컨트롤러 클래스 내부해당 컨트롤러 안에서 일어나는 예외만 잡을 수 있어 공통 코드가 늘어납니다.
@RestControllerAdvice애플리케이션 전체 컨트롤러전역에서 발생하는 모든 예외를 한곳에서 일관되게 관리할 수 있어 권장됩니다.
BasicErrorControllerSpring Boot 기본 제공 영역필터 계층 오류나 핸들러 매핑 전 오류 등 전역 서블릿 영역의 예외를 처리합니다.
광고

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

광고

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

FAQ

Q1. @ControllerAdvice와 @RestControllerAdvice의 명확한 차이점은 무엇인가요?

A1. 가장 큰 차이점은 응답을 반환하는 방식에 있습니다. @ControllerAdvice는 기본적으로 뷰(HTML 웹 페이지) 이름을 반환하여 화면을 렌더링할 때 주로 사용되고, @RestControllerAdvice는 내부적으로 @ResponseBody가 포함되어 있어 자바 객체를 즉시 JSON 포맷의 데이터 스트림으로 변환하여 클라이언트에게 전송해 줍니다. REST API 환경에서는 후자를 사용하는 것이 표준적입니다.

Q2. Filter(필터) 계층에서 발생하는 예외도 @RestControllerAdvice로 처리가 가능한가요?

A2. 아쉽게도 처리할 수 없습니다. Spring의 DispatcherServlet 뒤편에 존재하는 컨트롤러 인터셉터나 컨트롤러 내부 영역과 달리, Filter는 Spring MVC 컨텍스트 외부(서블릿 컨테이너 영역)에서 동작합니다. 따라서 필터에서 발생한 예외는 @RestControllerAdvice에 도달하지 못하므로, 별도의 Custom Filter 내부에서 ObjectMapper를 사용하여 에러 응답을 직접 쓰거나 ErrorController 영역으로 위임해 주어야 합니다.

Q3. 여러 개의 ExceptionHandler 메서드가 동일한 예외를 처리할 수 있다면 어떤 것이 실행되나요?

A3. Spring 프레임워크는 언제나 더 구체적이고 좁은 범위의 예외 클래스 타입을 선언한 메서드에 우선순위를 부여합니다. 만약 부모 클래스인 RuntimeException을 처리하는 메서드와 자식 클래스인 NullPointerException을 처리하는 메서드가 동시에 선언되어 있다면, 실제 NullPointerException이 발생했을 때 자식 타입을 선언한 매핑 메서드가 선택되어 실행됩니다.

광고

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

핵심키워드: Spring Boot Custom Exception 설계와 비즈니스 예외 처리 전략

관련 키워드

#spring boot rest api 예외 처리#spring boot rest api exception handling#spring boot rest exception handler example#spring rest api 예외 처리#@RestControllerAdvice#@ExceptionHandler#ErrorResponse#Spring Boot 에러 코드
목록