Backend/spring (Boot)

Spring boot 예외 처리: @ControllerAdvice, @ExceptionHandler, ResponseStatusException

dddzr 2025. 7. 26. 16:16

 

기존에 controller에서 예외 catch해서 직접 처리하는 방식을 자주 사용했었다.

@GetMapping("/user/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id) {
    try {
        User user = userService.findById(id);
        return ResponseEntity.ok(new ApiResponse<>("SUCCESS", "조회 성공", user));
    } catch (UserNotFoundException e) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(new ApiResponse<>("FAIL", e.getMessage(), null));
    }
}

근데 controller 마다 비슷한 예외 리턴해서 코드가 중복되어서 글로벌 처리로 고치고자 했다.

 

글로벌 예외 처리 방법에는

  • @ResponseStatus: 예외마다 별도의 사용자 정의 예외 클래스를 하나씩 생성
  • @ControllerAdvice + @ExceptionHandler: 응답 포맷을 일관되게 관리하고 싶을 때 (보통 REST API)
  • ResponseStatusException: 간단하게 처리하고 싶은 경우 (테스트나 단발성 예외 처리)

 

사용한 사용자 정의 예외 클래스가 많아지는 건 싫고 ResponseStatusException는 상태 코드와 함께 간단히 처리 가능한데

에러 응답 확장성을 위해서는 @ControllerAdvice + @ExceptionHandler을 추천한다고 한다.

 

🔍 1. @ControllerAdvice란?

  • 전역 예외 처리 클래스를 정의할 때 사용
  • 모든 컨트롤러에서 발생하는 예외를 한 곳에서 처리 가능
@ControllerAdvice 
public class GlobalExceptionHandler {
}

 

🔍 2. @ExceptionHandler란?

  • 예외 처리용 메서드를 정의할 때 사용
  • 특정 예외가 발생했을 때, 해당 메서드가 호출됨
  • 일반 컨트롤러 클래스 내부에도 쓸 수 있다. ( 컨트롤러 내부에서만 작동함!! )
  • @ControllerAdvice 클래스 내부에 쓰면 전역적으로 작동함.
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<Map<String, String>> handleUserNotFound(UserNotFoundException e) {
        Map<String, String> response = new HashMap<>();
        response.put("status", "FAIL");
        response.put("message", e.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, String>> handleGenericException(Exception e) {
        Map<String, String> response = new HashMap<>();
        response.put("status", "ERROR");
        response.put("message", "알 수 없는 오류가 발생했습니다.");
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
    }

 

🔍3. ResponseStatusException이란?

ResponseStatusException은 Spring Web에서 HTTP 상태 코드와 함께 예외를 던지고 싶을 때 사용하는 표준 예외 클래스.

Spring MVC 컨트롤러나 서비스에서 특정 상황에 대해 직접 HTTP 상태 코드를 지정해서 예외를 던질 수 있게 해준다.

  • HTTP 상태코드를 명시적으로 설정 가능 ➡️ @ResponseStatus가 필요 없음
  • REST API 응답 형식 통일 ➡️ JSON 오류 메시지 응답이 쉬워짐
  • Spring의 ExceptionHandler에서 자동 인식 ➡️ 따로 커스텀 예외를 만들지 않아도 됨
  • 컨트롤러에서 바로 throw 가능 ➡️ 비즈니스 로직 안에서도 던질 수 있음

 

3.1. 기본 사용법

throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "구글 인증 정보가 유효하지 않습니다.");

  • HttpStatus.UNAUTHORIZED → HTTP 응답 상태 401을 의미
  • "구글 인증 정보가 유효하지 않습니다." → 클라이언트에게 보낼 메시지

📖 사용 예제

@GetMapping("/events")
public List<Map<String, Object>> getEvents(@RequestHeader(value = "X-Google-Access-Token", required = false) String accessToken) {
    if (accessToken == null || accessToken.isEmpty()) {
        throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "액세스 토큰이 없습니다.");
    }

    try {
        return googleCalendarService.getGoogleCalendarList(accessToken);
    } catch (HttpClientErrorException.Unauthorized e) {
        throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "구글 인증 정보가 유효하지 않습니다.");
    }
}

 

📌 4. ControllerAdvice랑 ResponseStatusException 같이 사용

근데 나는 아직 그렇게까지 예외 커스텀할 필요를 못 느꼈다. 상태코드 + 메세지만 던지면 충분함!!

그래서 일단 예외처리 컨트롤러를 만들어 두고(@ControllerAdvice), ResponseStatusException을 throw하려고 했더니

 

  • GlobalExceptionHandler에서 Exception.class로 다 잡으면 이 예외도 500으로 변질됨
    ➜ 그래서 ResponseStatusException은 명시적으로 처리하거나, 다시 throw 해줘야 한다.
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(ResponseStatusException.class)
    public void handleResponseStatusException(ResponseStatusException e) {
        throw e;
    }
   
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, String>> handleAllExceptions(Exception e) {
        Map<String, String> response = new HashMap<>();
        response.put("error", "서버 에러");
        response.put("message", e.getMessage() != null ? e.getMessage() : "알 수 없는 오류가 발생했습니다.");
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}