oguri's garage

@ControllerAdvice와 @ExceptionHandler로 전역 예외 처리하기 본문

개발하다/Spring

@ControllerAdvice와 @ExceptionHandler로 전역 예외 처리하기

oguri 2025. 10. 16. 18:02

Spring에서 예외 처리가 어떻게 동작하는지를 실무 관점에서 정리합니다.
서비스 계층에서 발생한 예외를 전역적으로 처리하고, 일관된 HTTP 응답으로 변환하는 과정을 중심으로 살펴봅니다.






예외 처리 흐름 요약

Service에서 예외 발생
    ↓ 
throw new CustomException("...")
    ↓ 
@ExceptionHandler가 예외를 캐치
    ↓ 
handleException() 메소드 실행
    ↓ 
ResponseEntity<ErrorResponse> 반환
    ↓ 
Spring이 JSON으로 변환 후 HTTP 응답






핵심 구조 설명

1. @ExceptionHandler - 예외를 처리할 메소드 지정

@ExceptionHandler({
    CustomException.class,
    DuplicateDataException.class,
    InvalidParameterException.class
})
public ResponseEntity<Object> handleException(Exception ex, WebRequest request)

 

@ExceptionHandler"이 예외들이 발생하면 이 메소드로 처리한다" 는 선언입니다.
배열 형태로 여러 예외 클래스를 지정할 수 있으며, 해당 예외가 발생하면 Spring이 자동으로 이 메소드를 실행합니다.



2. 예외 타입별 응답 분기

if (ex instanceof CustomException subEx) {
    errorResponse = subEx.getErrorResponse();
    statusCode = HttpStatus.BAD_REQUEST;
} else if (ex instanceof DuplicateDataException subEx) {
    errorResponse = subEx.getErrorResponse();
    statusCode = HttpStatus.CONFLICT;
}


하나의 핸들러 메소드에서 여러 예외를 처리하면서도, 각 예외의 성격에 맞는 HTTP 상태 코드를 반환할 수 있습니다.
공통 로직은 유지하면서 세부 응답만 다르게 처리하는 구조입니다.



3. HTTP 응답 생성

return new ResponseEntity<>(errorResponse, new HttpHeaders(), statusCode);

 

ResponseEntity를 사용하면 HTTP 상태 코드, 헤더, 응답 본문을 명시적으로 제어할 수 있습니다.
Spring이 이를 자동으로 JSON으로 직렬화하여 클라이언트에게 전달합니다.






예시 코드

Service 계층에서 예외 발생

@Service
public class UserService {
    public User findUser(String id) {
        User user = repository.findById(id);
        if (user == null) {
            throw new CustomException("User not found: " + id, "USER_NOT_FOUND");
        }
        return user;
    }
}



Global Exception Handler 구현

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(CustomException.class)
    public ResponseEntity<Object> handleCustomException(
        CustomException ex, 
        WebRequest request
    ) {
        ErrorResponse body = new ErrorResponse(
            ex.getCode(),
            ex.getMessage(),
            request.getDescription(false)
        );
        return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
    }
}



클라이언트가 받는 응답

{
    "errorCode": "USER_NOT_FOUND",
    "message": "User not found: abc123",
    "path": "/api/user/abc123",
    "method": "GET"
}







CustomException 설계

기본 구조

public class CustomException extends RuntimeException {
    private final String code;

    public CustomException(String message, String code) {
        super(message);
        this.code = code;
    }

    public String getCode() {
        return code;
    }

    public ErrorResponse getErrorResponse() {
        return new ErrorResponse(code, getMessage());
    }
}



Exception vs RuntimeException 선택

Custom 예외를 만들 때 ExceptionRuntimeException 중 어떤 것을 상속할지 선택해야 합니다.

 

 

구분 Exception (Checked) RuntimeException (Unchecked)
예외 처리 강제 여부 컴파일 타임에 명시적으로 처리해야 함 처리 강제 없음
사용 목적 예상 가능한 예외 (비즈니스 로직상 예외) 프로그래밍 오류나 예상치 못한 상황
코드 안정성 호출부에서 반드시 예외 처리 필요 단순하고 유연하지만 처리 누락 가능
추천 케이스 "비즈니스 오류"를 명확히 표현할 때 대부분의 REST API 서비스 로직



RuntimeException을 주로 사용

@ControllerAdvice를 통한 전역 예외 처리 환경에서는 RuntimeException을 상속하는 방식 이 더 일반적입니다.


Unchecked 예외여도 @ControllerAdvice가 자동으로 처리하기 때문에, 매번 throws 선언이나 try-catch를 강제할 필요가 없습니다.

// 대부분의 REST API에서 사용하는 방식
public class CustomException extends RuntimeException {
    private final String code;

    public CustomException(String message, String code) {
        super(message);
        this.code = code;
    }
}



Checked 예외를 사용하는 경우

반면 핵심 도메인 로직이나 외부 시스템 연동처럼 예외 처리를 명시적으로 강제해야 하는 상황에서는 Exception 기반의 checked 예외도 유용합니다.

// 중요한 비즈니스 로직에서 예외 처리를 강제
public void processPayment(Order order) throws PaymentException {
    // 호출하는 쪽에서 반드시 예외 처리를 해야 함
}







동작 원리: Service 예외를 어떻게 처리하는가?

예외 전파 메커니즘

Spring MVC에서 요청이 처리되는 흐름은 다음과 같습니다:

[요청 흐름]
Client → Controller → Service → Repository

[예외 발생 시 전파 흐름]
Repository (예외 발생)
    ↓
Service (예외 전파)
    ↓
Controller (예외 전파)
    ↓
DispatcherServlet
    ↓
@ControllerAdvice가 감지하여 처리

 

Java에서 예외가 발생하면 호출 스택을 거슬러 올라가며 전파됩니다.
Service에서 발생한 예외는 이를 호출한 Controller로 전파되고, Controller에서 처리되지 않으면 최종적으로 Spring의 DispatcherServlet까지 도달합니다.

 

@ControllerAdvice는 이 시점에 개입하여 예외를 가로채고 처리합니다.
즉, Controller 계층을 벗어나기 직전의 마지막 방어선 역할을 하는 것입니다.



전역 예외 처리가 가능한 이유

@ControllerAdvice  // 모든 @Controller에 대해 적용
public class GlobalExceptionHandler {

    @ExceptionHandler(CustomException.class)  // 이 예외 타입을 감지
    public ResponseEntity<Object> handleCustomException(CustomException ex) {
        // 통합된 응답 생성
    }
}

 

@ControllerAdvice는 Spring이 제공하는 AOP(Aspect-Oriented Programming) 기반의 횡단 관심사 처리 방식입니다.
모든 Controller를 감싸는 계층으로 동작하며, 각 Controller에서 예외 처리 코드를 중복 작성하지 않아도 됩니다.

 

이를 통해:

  • Service, Repository 어디서 예외가 발생하든 일관된 처리 가능
  • Controller마다 try-catch 블록을 작성할 필요 없음
  • 예외 처리 로직을 한 곳에서 관리 가능






정리

  • Service 계층에서 발생한 예외는 호출 스택을 따라 Controller로 전파되고, @ControllerAdvice가 이를 감지하여 처리합니다.
  • @ExceptionHandler를 통해 특정 예외 타입마다 다른 응답을 생성할 수 있습니다.
  • HTTP 상태 코드와 JSON 응답 형식을 일관성 있게 관리할 수 있습니다.
  • RuntimeException 상속이 일반적이지만, 상황에 따라 checked 예외도 선택할 수 있습니다.

이 구조를 설정해두면 전역적으로 예외를 통합 관리할 수 있고, 클라이언트는 항상 정의된 형식의 에러 응답을 받을 수 있습니다.






참고 자료