안녕하세요. 이번 포스팅은 스프링에서 원활하게 예외처리를 할 수 있도록 제공해주는 ExceptionResolver에 대해 알아보겠습니다.
스프링에서 예외가 발생하면 서블릿을 넘어 WAS까지 예외가 전달되며 HTTP 상태코드가 500으로 처리됩니다.
하지만, 발생하는 예외에 따라서 400, 401, 403, 404 등으로 각각 처리해야 하는 경우도 존재합니다.
이럴 때 HandlerExceptionResolver 인터페이스를 활용함으로써 각 예외마다 원하는 HTTP 상태코드를 지정할 수 있습니다.
HandlerExceptionResolver 인터페이스를 구현하고 resolveException 메서드를 오버라이딩하여 커스터마이징을 하면 됩니다.
하지만 굳이 커스터마이징을 할 필요없이 스프링에서는 3가지의 ExceptionResolver를 제공해줍니다.
각각 우선 순위가 존재하며 차례대로 우선 순위가 높은 순입니다.
1. ExceptionHandlerExceptionResolver
2. ResponseStatusExceptionResolver
3. DefaultHandlerExceptionResolver
ExceptionHandlerExceptionResolver
@ExceptionHandler 어노테이션이 붙은 메서드를 찾아 예외를 처리합니다.
그리고 @ControllerAdvice 또는 @RestControllerAdvice를 사용해서 비즈니스 로직과 예외처리 코드를 분리시켜 운영할 수도 있습니다.
@ExceptionHandler 예제
// ApisExceptionHandler.class
@RestControllerAdvice // 대상을 지정하지 않으면 모든 컨트롤러에 적용된다.(글로벌)
//@RestControllerAdvice(annotations = RestController.class) // RestController한테만 적용
//@RestControllerAdvice(패키지) // 지정한 패키지 내 컨트롤러에만 적용
//@RestControllerAdvice(assignableTypes = {A.class, B.class}) // 직접 어떤 컨트롤러한테 적용할 때
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult IllegalArgumentExceptionHandler(IllegalArgumentException e) {
// 현재 컨트롤러에서 IllegalArgumentException을 상속받은 클래스의 예외까지 잡아준다.
return new ErrorResult("파라미터 누락", e.getMessage());
}
@ExceptionHandler(UserException.class)
public ResponseEntity<ErrorResult> userExceptionHandler(UserException e) {
// 현재 컨트롤러에서 UserException을 상속받은 클래스의 예외까지 잡아준다.
ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
// Http 응답 코드를 동적으로 변경할 수 있다.
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public ErrorResult exceptionHandler(Exception e) {
// 나머지 예외는 현재 메서드가 잡는다.
return new ErrorResult("EX", "내부오류");
}
}
컨트롤러를 지정하는 방식은 단일 컨트롤러 지정하는 방식, 패키지 지정하는 방식 등 여러 가지가 존재하여 필요한 방식을 골라서 사용하시면 됩니다.
@ResponseStatus를 왜 쓸까?
IllegalArgumentExceptionHandler, exceptionHandler 메서드에는 @ResponseStatus 어노테이션이 추가로 달려있는 것을 확인할 수 있습니다.
ExceptionResolver을 사용하기 전에는 스프링 예외가 발생 시 동작 메커니즘은 컨트롤러 예외 발생 -> 서블릿 -> WAS -> 에러코드 처리할 컨트롤러 탐색 -> 에러 컨트롤러 실행이었습니다.
하지만 ExceptionResolver를 사용하고 스프링 예외가 발생하면 컨트롤러 예외 발생 -> @ExceptionHandler 탐색 -> 정상 흐름으로 동작합니다.
즉, 정상 흐름으로 동작한다는 것은 HTTP 응답코드를 200으로 내려준다는 것인데, 예외가 발생해도 HTTP 응답코드를 200으로 내려주는 것은 정상적인 응답 결과가 아니기 때문에 @ResponseStatus 어노테이션을 사용해서 HTTP 응답코드를 지정해주는 것입니다.
ResponseStatusExceptionResolver
@ExceptionHandler 어노테이션이 붙은 메서드가 없다면 두 번째 우선순위인 ResponseStatusExceptionResolver가 동작하면서 예외를 처리하게 됩니다.
ResponseStatusExceptionResolver는 예외에 따라서 HTTP 상태 코드를 지정해주는 역할을 합니다.
다음 두 가지 경우를 처리합니다.
1. @ResponseStatus 어노테이션이 달려있는 예외
2. ResponseStatusException 예외
@ResponseStatus
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}
BadRequestException 예외가 컨트롤러에서 발생하면 ResponseStatusExceptionResolver 예외가 해당 어노테이션을 확인해서 오류 코드를 HttpStatus.BAD_REQUEST로 변경하고, 메시지도 담습니다.
소스코드를 내부까지 살펴보면 결국 response.sendError(code, reason)을 호출함으로써 response 객체에 강제로 에러를 설정하는 것을 확인할 수 있습니다.

참고로 위 사진에서 보시다시피 @ResponseStatus 어노테이션의 reason 키에는 문자열을 직접 입력하기도 하지만, MessageSource에서 찾는 기능도 제공합니다.
ResponseStatusException
@ResponseStatus 어노테이션을 사용한 경우는 개발자 직접 RuntimeException 클래스를 상속받아 예외 클래스를 만든 경우만 해당됩니다.
하지만 개발을 하다보면 IllegalArgumentException처럼 자바나 스프링에서 만들어 놓은 예외 클래스를 사용하는 경우가 있습니다.
이러한 클래스들은 수정이 불가능하기 때문에 @ResponseStatus를 적용할 수가 없습니다.
이럴 때 ResponseStatusException 클래스를 사용함으로써 해결할 수 있습니다.
@GetMapping("/response-status-ex-test")
public String responseStatusException() {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "존재하지 않음", new IllegalArgumentException()
);
}
위처럼 IllegalArgumentException 클래스를 ResponseStatusException 클래스의 파라미터에 넣어주고, HTTP 응답코드와 메시지를 정의해서 ResponseStatusException 클래스를 생성하면 IllegalArgumentException 예외가 발생했을 때 지정한 HTTP 응답코드와 메시지가 json 형식으로 리턴됩니다.
DefaultHandlerExceptionResolver
DefaultHandlerExceptionResolver는 스프링 내부에서 발생하는 스프링 예외를 해결합니다.
대표적으로 파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException 이 발생하는데, 이 경우 예외가 발생했기 때문에 그냥 두면 서블릿 컨테이너까지 오류가 올라가고, 결과적으로 500 오류가 발생하게 됩니다.
그런데 파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출해서 발생하는 문제이므로 HTTP 응답코드 400을 리턴해야 합니다.
DefaultHandlerExceptionResolver 는 이러한 경우 500 오류가 아니라 HTTP 상태 코드 400 오류로 자동으로 변경해주는 역할을 합니다.
가장 편리하게 스프링 예외를 다루는 방법은 ExceptionHandlerExceptionResolver라고 생각합니다.
다만, 서버 에러를 핸들링하기 위해서 나머지 우선순위의 ExceptionResolver도 알아둬야 한다고 생각합니다.
잘못된 내용이 있다면 댓글 부탁드리겠습니다. 읽어주셔서 감사합니다:)