스프링에서 예외를 잘! 처리하는 방법
Intro
자바 프로그램에서 예외가 발생하면 스레드가 종료됩니다. 하지만 스프링에서는 예외가 발생해도 어플리케이션이 종료되지 않고, 예외 응답을 반환합니다. 스프링은 어떻게 예외를 처리하기에 이렇게 작동하는 것일까요?🤔 이번 글에서는 스프링에서 어떻게 예외를 처리하는지와, 어떻게해야 예외를 잘~ 처리할 수 있는지 알아보겠습니다.
🔍 스프링의 기본 예외 처리 방식
어떠한 예외처리도 하지 않았을 때, 스프링에서 어떤식으로 예외가 처리될까요?
우선 스프링의 기본 작동 방식에 대해 생각해봅시다.
스프링에 요청이 오면, WAS - Dispatcher Servlet - HandlerMapping - HandlerAdapter - Controller
를 거쳐 로직이 실행됩니다.
이때 예외가 발생하면 예외 내용은 WAS까지 거슬러 올라갑니다.
그럼 WAS는 어플리케이션에서 처리할 수 없는 예외라 판단하여 에러 컨트롤러로 예외 내용을 전달합니다.
즉, 예외가 발생하면 WAS - Controller - WAS - ErrorController
의 흐름을 갖게 됩니다.
이 과정을 통해 예외가 발생해도 어플리케이션을 종료하지 않고, 마치 정상 요청인 것처럼 예외 응답을 반환하는 것입니다.
BasicErrorController
이때 호출되는 에러 컨트롤러가 바로 BasicErrorController
입니다.
실제로 어떠한 예외처리도 하지 않고 BasicErrorController에 BreakPoint 를 걸어두고 디버깅하니,
BasicErrorController에서 예외가 핸들링되는 것을 확인할 수 있었습니다.
BasicErrorController의 응답
BasicErrorController는 DefaultErrorAttributes
의 getErrorAttributes()
함수를 호출해서 응답할 내용을 불러옵니다.
이 때 getErrorAttributes()가 기본적으로 제공하는 속성과, 설정을 통해 추가할 수 있는 속성들은 다음과 같습니다.
- timestamp: 에러가 발생한 시간
- status: 에러의 Http 상태
- error: 에러 코드
- path: 에러가 발생한 uri
- exception: 최상위 예외 클래스의 이름(설정 필요)
- message: 에러에 대한 내용(설정 필요)
- errors: BindingExecption에 의해 생긴 에러 목록(설정 필요)
- trace: 에러 스택 트레이스(설정 필요)
// 어떤 설정도 하지 않았을 때의 응답
{
"timestamp": "2024-07-21T16:16:40.463+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/reviews/999"
}
// 추가할 수 있는 속성들
server:
error:
include-message: always
include-binding-errors: always
include-stacktrace: always
include-exception: true
// 모든 속성을 추가했을 때의 응답
{
"timestamp": "2024-07-21T16:19:58.729+00:00",
"status": 500,
"error": "Internal Server Error",
"exception": "org.springframework.orm.jpa.JpaObjectRetrievalFailureException",
"trace": "org.springframework.orm.jpa.JpaObjectRetrievalFailureException\n\tat ….",
"message": "No message available",
"path": "/reviews/999"
}
BasicErrorController를 통한 예외 처리의 한계
혹시 '이것만으로도 충분히 훌륭한데?' 라는 생각이 드시나요? 하지만 이 방식에는 몇가지 한계가 존재합니다. 첫째로, 이 방식은 WAS에서 컨트롤러를 거쳐, 다시 WAS로 왔다가, 에러 컨트롤러로 가는 흐름인데요, 이 과정이 길고 복잡하게 느껴집니다. 또 이는 필터나 인터셉터를 2번 호출하는 등 다른 문제를 야기할 수 있습니다. 그리고 결정적으로, 예외에 따라 다른 HttpStatusCode와 메세지를 줄 수도 없습니다😓
🔍 스프링이 제공하는 다양한 예외 처리 방식
이런 단점을 보완하기 위해서, 스프링에서는 기본 예외 처리 외에도 다양한 예외 처리 방식을 제공합니다.
HandlerExceptionResolver
HandlerExceptionResolver는 예외 처리 방식을 추상화한 인터페이스입니다. 이를 구현하는 구현체들은 발생한 예외를 캐치하여 응답의 Http 상태나 메세지를 설정합니다.
HandlerExceptionResolver의 구현체들
HandlerExceptionResolver의 구현체들은 HandlerExceptionResolverComposite에 우선순위 순서대로 빈으로 등록되어 관리됩니다.
그리고 예외 발생 시, 우선순위 순으로 구현체들을 순회하며 핸들링을 할 수 있는지 확인합니다.
public class HandlerExceptionResolverComposite implements HandlerExceptionResolver, Ordered {
@Override
@Nullable
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
if (this.resolvers != null) {
// 구현체들을 순회하면서 예외를 처리할 수 있는지 확인
for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (mav != null) {
return mav;
}
}
}
return null;
}
}
빈으로 등록된 HandlerExceptionResolver 구현체과 그것이 처리하는 대상은 아래와 같습니다.
- ExceptionHandlerExceptionResolver : ExceptionHandler를 처리
- ResponseStatusExceptionResolver : ResponseStatus, ResponseStatusException를 처리
- DefaultHandlerExceptionResolver : 스프링 내부의 기본 예외들을 처리
하지만 ResponseStatusExceptionResolver
만으로는 에러 응답을 수정할 수 없으며, 일괄 에러 처리를 할 수 없다는 단점이 있으므로 이번 글에서는 설명하지 않겠습니다.
🔍 HandlerExceptionResolver를 동작시키는 것들
@ExceptionHandler와 @ControllerAdvice
@ExceptionHandler는 가장 우선순위가 높은 예외 핸들러인 ExceptionHandler ExceptionResolver에서 처리됩니다. @ExceptionHandler는 예외 응답의 코드와 메세지를 자유롭게 설정할 수 있다는 장점이 있습니다. @ExceptionHandler는 컨트롤러의 메서드에 바로 사용할 수도 있으며, @ControllerAdvice나 @RestControllerAdvice가 있는 클래스의 메소드에도 사용할 수 있습니다. 만약 컨트롤러의 메서드에도 @ExceptionHandler를 사용하고, @ControllerAdvice에도 사용한다면 전자가 우선시됩니다.