ExceptionHandler
API 통신을 사용할 때는 다양한 오류 상황에 맞는 스펙으로 응답을 내려주어야 한다. 또한, 에러 응답은 일관적이어야 한다. 매 요청마다 일관적이지 못한 응답 스펙이 오면 클라이언트 입장에서 에러 응답을 처리하기가 어렵기 때문이다.
스프링에서 이를 편리하게 처리할 수 있도록 제공하는 것이 @ExceptionHandler
이다.
공통 예외 처리
@ExceptionHandler
를 이용한 공통 예외 처리 코드를 먼저 제시하고 각 코드들의 역할에 대해 설명하겠다.
@ExceptionHandler
@ExceptionHandler 애노테이션을 메서드 위에 선언하고 처리하고자 하는 예외를 명시하면 해당 예외가 발생했을 때 이 메서드를 호출한다.
처리하고자하는 예외를 무조건 등록할 필요는 없다. 등록하지 않으면 메서드 시그니처에 명시된 예외를 기본값으로 수행한다.
예를 들어, 위 예제의 handleUserException 메서드는 UserException 예외가 발생하면 호출된다.
@ResponseStatus
별다른 응답 메시지 설정 없이 그냥 예외 응답을 보내면 어떻게 될까?
위 ErrorResponse 객체는 사용자가 임의로 만든 객체이기 때문에 이 응답이 오류인지 정상인지 알 수가 없다. 따라서 200 OK 응답이 내려가게 된다.
하지만 오류라면 명시된 에러 상태 코드를 반환해야 한다. 이를 명시적으로 지정해주는 것이 @ResponseStatus
이다.
이 애노테이션에 상태코드를 명시하여 오류 상태 코드도 메시지에 함께 넣어 줄 수 있다.
@ControllerAdvice
모든 컨트롤러마다 @ExceptionHandler
를 구현하는 것은 비효율적이다. 이를 개선한 방식이 @ControllerAdvice
이다.
@ControllerAdvice
는 대상으로 지정한 컨트롤러에 @ExceptionHandler
, @InitBinder
를 부여해주는 역할을 한다.
대상을 지정하지 않으면 모든 컨트롤러에 전역적으로 예외 처리를 적용할 수 있다.
동작 원리 (HandlerExceptionResolver)
어떻게 특정 컨트롤러에서 예외가 발생했을 때 이를 찾아 JSON 객체를 응답해주는 것일까? 이것을 알아보기 전에 기본 에러 응답에 대해 먼저 알아보자.
우리는 이런 에러 응답을 많이 받아본 적이 있다.
하지만 이 응답은 우리가 따로 만들어준 적이 없다. 이 응답은 다음과 같은 과정을 통해 생성된다.
처음 API 콜을 통해 컨트롤러에 전달이되고 해당 컨트롤러의 로직이 수행되다 예외가 발생한다. 이후 WAS에 예외가 전달되면 WAS에서 /error
로 다시 호출한다.
/error
를 경로로 가지고 있는 BasicErrorController
에서 error()
메서드를 호출해 응답 메시지를 생성하고 이를 WAS 전달하고 마무리 된다.
모든 예외가 발생할 때마다 /error
를 호출하고 상태 코드 500을 사용하는 것은 복잡하다.
이를 해결하기 위해 스프링은 컨트롤러 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법인 HandlerExceptionResolver
을 제공한다.
스프링에서 제공하는 HandlerExceptionResolver
는 다음과 같다. HandlerExceptionResolverComposite
에 순서대로 등록하며 순서대로 우선 순위가 적용된다.
ExceptionHandlerExceptionResolver
ResponseStatusExceptionResolver
DefaultHandlerExceptionResolver
우선 순위가 가장 낮다.
쉬운 순서대로 알아보자.
ResponseStatusExceptionResolver
ResponseStatusExceptionResolver
는 예외에 따라서 HTTP 상태 코드를 지정해 주는 역할을 한다.
@ResponseStatus
가 붙어있거나 ResponseStatusException
을 찾아 처리한다.
response.sendError()
를 호출하는데 이 메서드를 수행하면 WAS에서 다시 오류 처리 요청(/error
)을 보낸다.
DefaultHandlerExceptionResolver
DefaultHandlerExceptionResolver
은 스프링 내부에서 발생하는 스프링 예외를 해결한다.
우리가 Integer
필드에 문자열을 넣어서 요청을 하면 500이 아닌 400 응답이 온다. 이 과정이 DefaultHandlerExceptionResolver
의 로직이 수행된 결과이다.
필드 타입 예외로 TypeMismatchException이 발생한다. DefaultHandlerExceptionResolver.doResolveException()
가 이 예외를 잡아 400 응답을 주도록 변경해주는 것이다.
DefaultHandlerExceptionResolver
또한 response.senderror()
를 사용하기 때문에 추가적으로 /error
를 호출한다.
ExceptionHandlerExceptionResolver
@ExceptionHandler에서 처리할 예외를 명시하고 해당 예외가 발생하면 메서드를 호출하는 로직을 수행했다.
예외가 발생했을 때, 해당 예외를 처리하는 @ExceptionHandler
을 찾아 로직을 수행시키는 역할을 수행한다.
위의 두 HandlerExceptionResolver와의 차이점은 /error
를 호출하지 않고 ExceptionHandlerExceptionResolver
에서 응답이 마무리 된다는 것이다.
동작 방식이 다음 그림과 같이 간소화 되었다.
결론
예외 처리를 쉽게 수행할 수 있도록 도와주는 ExceptionHandler + Resolver에 대해 알아보았다. 이 기술을 활용하면 실무에서도 공통적인 예외를 쉽게 처리할 수 있을 것이다.
이 기술을 활용한 코드는 예외 처리 가이드에서 확인할 수 있다.