🧐 서론
스프링에서는 컨트롤러에서 발생한 예외를 편리하게 처리할 수 있도록 @ExceptionHandler 와 @ControllerAdvice를 제공해줍니다.
이 둘의 사용 방법이 아니라 어떤 식으로 동작하는지에 대해서 내부 코드를 살펴가며 알아보도록 하겠습니다.
🧐 스프링의 초기화 과정
우선 스프링의 초기화 과정부터 살펴보겠습니다.
SpringAppliction의 run 메서드 내부에서, refreshContext()를 통해 빈들을 초기화하고 의존관계를 설정하는 과정을 거칩니다.
메서드의 호출을 따라가다보면 AbstractApplicationContext의 refresh()를 호출하게 됩니다.
이곳에서는 finishBeanFactoryInitialization() 메서드를 통해 이전 과정들에서 등록되지 않은 빈들을 등록해주는 과정을 거칩니다.
(저희가 등록한 빈들도 대부분 해당 과정에서 등록됩니다.)
해당 메서드 내부에서는 이런 저런 처리를 거친 후, beanFactory.preInstantiateSingletons() 메서드를 호출하는데, 해당 메서드는 다음과 같습니다.
getBean() 메서드에서 최종적으로 빈이 등록됩니다.
🧐 ExceptionHandler의 초기화 - WebMvcConfigurationSupport
위의 getBean() 메서드에 HandlerExceptionResolver가 걸린 경우, HandlerExceptionResolver 타입의 빈을 생성하기 위해 최종적으로 WebMvcConfigurationSupport의 handlerExceptionResolver 메서드를 호출합니다.
사용자가 따로 설정하지 않는다면, addDefaultHandlerExcecptionResolvers에 의해 아래와 같은 3개의 ExceptionResolver가 등록됩니다.
등록되는 과정을 자세히 보기 위해 addDefaultHandlerExceptionResolvers 의 내부를 보도록 하겠습니다.
위와 같이 ExceptionHandlerExceptionResolver와, ResponseStatusExceptionResolver, DefaultHandlerExceptionResolver가 등록되는 것을 알 수 있습니다.
이때 @ExceptionHandler의 처리는 ExceptionHandlerExceptionResolver에서 담당하므로, 해당 부분을 자세히 보겠습니다.
기본적인 설정이 끝나고 호출되는 afterPropertiesSet의 내부를 살펴보겠습니다.
initExceptionHandlerAdviceCache() 내부에서는 @ControllerAdvice가 붙은 빈을 우선 찾습니다.
이후 찾아진 빈들에 타입 토큰을 매개변수로 각각 ExceptionHandlerMethodResolver의 생성자를 호출하는데, 다음과 같이 구현되어있습니다.
먼저 MethodIntrospector.selectMethods()를 통해 해당 빈에서 @ExceptionHandler가 붙은 메서드를 가져옵니다.
이후 detectExceptionMappings를 통해 @ExceptionHandler가 처리하는 예외의 타입을 모두 가져옵니다.
그리고 해당 예외 타입과, 이를 처리하는 메서드를 해당 메서드를 mappedMethods 필드에 추가합니다.
위 과정이 끝난 경우 다시 ExceptionHandlerExceptionResolver.initExceptionHandlerAdviceCache()로 돌아와서,
@ControllerAdvice가 붙은 빈과, 방금 생성한 ExceptionHandlerMethodResolver를 exceptionHandlerAdviceCache에 저장합니다.
이 과정을 ControllerAdvice가 붙은 클래스들 전부에 대해 진행합니다.
이후 남은 자잘한 과정들을 거치면 ExceptionHandlerExceptionResolver가 생성되고, 나머지 두 기본 ExceptionResolver도 생성이 진행된다면 WebMvcConfigurationSupport의 addDefaultHandlerExceptionResolvers가 끝납니다.
WebMvcConfigurationSupport의 handlerExceptionResolver() 으로 돌아와서,
이렇게 추가된 ExceptionResolver들은 HandlerExceptionResolverComposite이라는 HandlerExceptionResolver에 등록됩니다.
HandlerExceptionResolverComposite는 Composite 패턴이 적용된 HandlerExceptionResolver입니다.
내부에 다음과 같이 HandlerExceptionResolver들을 가지고 있으며, 이들을 통해 예외를 해결합니다.
이때 처음 HandlerExceptionResolver가 예외를 처리했다면, 이후 Resolver는 호출되지 않도록 구현되었습니다.
이렇게 해서 최종적으로 handlerExceptionResolver 빈으로 HandlerExceptionResolverComposite가 등록되며 초기화가 완료됩니다
🧐 요청을 처리하는 과정에서 예외처리
이제 이러한 HandlerExceptionResolverComposite가 실제 어디서, 어떻게 사용되는시 살펴보겠습니다.
우선 요청이 들어오면 DispatcherServlet을 거쳐서 컨트롤러가 호출됩니다.
요청이 들어오면 DispacherServlet의 doDispatch 메서드에서 다음과 같은 과정을 거치게 됩니다.
이때 컨트롤러에서 예외가 발생한 경우, 즉 ha.handle() 내부에서 예외가 발생한다면, 해당 예외는 processDispatchResult에 전달됩니다.
이후 processHandlerException을 호출하는데, 이곳에서 아까 설정된 HandlerExceptionResolverComposite를 사용하여 예외를 처리합니다.
HandlerExceptionResolverComposite.resolveException()은 위에서도 살펴보았지만 다시 살펴보자면, 다음과 같이 구현되어 있습니다.
이곳에서 처음으로 실행되는 것이 ExceptionHandlerExceptionResolver입니다.
ExceptionHandlerExceptionResolver는 은 AbstractHandlerMethodExceptionResolver를 상속받아 구현되었으며, AbstractHandlerMethodExceptionResolver는 AbstractHandlerExceptionResolver를 상속받았습니다.
AbstractHandlerExceptionResolver의 기본 resolveException의 골격은 다음과 같습니다.
doResolveException은 AbstractHandlerMethodExceptionResolver에서 구현되어 있습니다.
최종적으로 ExceptionHandlerExceptionResolver의 doResolveHandlerMethodException()이 호출됩니다.
ExceptionHandlerExceptionResolver의 doResolveHandlerMethodException()는 핸들러(컨트롤러)에서 발생한 예외를 처리하는 메서드를 가지고 온 뒤에, 해당 메서드를 호출하도록 구현되었습니다.
이때, 핸들러에서 발생한 예외를 처리하는 메서드를 가지고 오는 getExceptionHandlerMethod는 다음과 같이 구현되어 있습니다.
@Controller 내부에서 사용되는 @ExceptionHandler가 먼저 작동할 수 있도록, exceptionHandlerCache를 먼저 찾은 뒤,
이곳에 없다면 exceptionHandlerAdviceCache를 찾아 ControllerAdvice에서 예외를 처리할 수 있도록 구현되어 있습니다.
이때 exceptionHandlerCache의 Key로는 현재 요청을 처리하는 @Controller가 붙은 클래스가,
Value로는 해당 클래스에 @ExceptionHandler를 통해 정의된 예외처리 메서드들이 등록되는 것을 볼 수 있습니다.
이러한 등록은, 첫 요청 시 해당 캐시에 존재하지 않는 경우에, computeIfAbsent의 두번째 인자로 new ExceptionHandlerMethodResolver(handlerType)를 수행함으로써 저장되는데요,
이때 다시 ExceptionHandlerMethodResolver의 생성자를 통해 @ExceptionHandler가 붙은 메서드를 가져와 등록하게 됩니다.
이렇게 해서 @ControllerAdvice의 동작과정을 살펴보았습니다.
'🏝️ Spring > Web MVC' 카테고리의 다른 글
[Test] 인수테스트 가독성 증가시키기 (0) | 2023.05.22 |
---|---|
[Spring MVC] 인터셉터는 어떻게 동작하는가(feat. HandlerExecutionChain, 인터셉터의 예외처리) (0) | 2023.04.21 |
[Spring MVC] 스프링 인터셉터 적용 시 NOT FOUND가 발생하지 않는 경우 (0) | 2023.01.21 |
[Spring MVC] @Controller에서 ResponseEntity를 사용하면 어떻게 되나? (0) | 2022.07.04 |
[Spring MVC] - @ReqeustBody가 값을 바인딩 할 수 있는 조건 (0) | 2022.06.27 |