티스토리 뷰
[Spring] ControllerAdvice는 AOP로 구현되어 있을까? ControllerAdvice의 동작 과정 살펴보기
망나니개발자 2022. 4. 28. 10:10이번에는 ControllerAdvice의 동작 과정이 어떻게 되는지 코드로 직접 살펴보도록 하겠습니다.
1. ControllerAdvice의 동작 과정 살펴보기
[ ControllerAdvice의 동작 과정 ]
- 디스패처 서블릿이 에러를 catch함
- 해당 에러를 처리할 수 있는 처리기(HandlerExceptionResolver)가 에러를 처리함
- 컨트롤러의 ExceptionHandler로 처리가능한지 검사함
- ControllerAdvice의 ExceptionHandler로 처리가능한지 검사함
- ControllerAdvice의 ExceptionHandler 메소드를 invoke하여 예외를 반환함
1. 디스패처 서블릿이 에러를 catch함
스프링에서 모든 요청을 가장 먼저 받는 곳은 디스패처 서블릿이다. 그러다보니 에러가 발생하면 에러 처리가 시작되는 곳 역시 디스패처 서블릿인데, 디스패처 서블릿의 핵심 메소드인 doDispatch에는 다음과 같이 모든 Exception과 Throwable을 catch하고 있다.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
// 요청을 컨트롤러로 위임하는 부분 생략
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
그리고 내부에서 exception이 null인지 아닌지 검사하여 exception이 존재하면 에러를 처리해주고 있다. 일반적으로 우리가 추가한 Exception들은 processHandlerException에서 처리가 될 것이다.
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// 생략
}
2. 해당 에러를 처리할 수 있는 처리기(HandlerExceptionResolver)가 에러를 처리함
다른 포스팅에서 디스패처 서블릿이 다양한 예외 처리기(HandlerExceptionResolver)를 가지고 있음을 확인하였다. 예외 처리 시에는 각가의 구현체들로부터 예외를 핸들링시키는데, 반환 결과가 null이 아니라면 정상적으로 처리된 것이다. HandlerExceptionResolver의 구현체 중에서 ControllerAdvice는 ExceptionHandlerExceptionResolver에 의해 처리된다.
@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;
}
3. 컨트롤러의 ExceptionHandler로 처리가능한지 검사함
ExceptionHandler는 Controller에 구현할 수도 있고, ControllerAdvice에도 구현할 수 있다. ControllerAdvice에 구현하는 것은 전역적인 반면에 Controller에 구현하는 것은 지역적이다. 그러므로 Controller에 있는 ExceptionHandler가 우선 순위를 갖도록 먼저 컨트롤러의 ExceptionHandler를 검사한다. 그리고 컨트롤러에 있는 ExceptionHandler가 예외를 처리할 수 있다면 (예외를 처리할 빈, 예외를 처리할 ExceptionHandler 메소드, 애플리케이션 컨텍스트)를 담은 ServletInvocableHandlerMethod를 만들어 반환한다. 여기서 예외를 처리할 빈에는 컨트롤러가 존재한다.
@Nullable
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
@Nullable HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = null;
if (handlerMethod != null) {
handlerType = handlerMethod.getBeanType();
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
if (resolver == null) {
resolver = new ExceptionHandlerMethodResolver(handlerType);
this.exceptionHandlerCache.put(handlerType, resolver);
}
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method, this.applicationContext);
}
// For advice applicability check below (involving base packages, assignable types
// and annotation presence), use target class instead of interface-based proxy.
if (Proxy.isProxyClass(handlerType)) {
handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
}
}
...
}
4. ControllerAdvice의 ExceptionHandler로 처리가능한지 검사함
컨트롤러에서 갖는 ExceptionHandler로 처리가 불가능하다면 등록된 모든 ControllerAdvice 빈을 검사한다. 그리고 처리 가능한 ControllerAdvice의 ExceptionHandler가 있다면 마찬가지로 ServletInvocableHandlerMethod를 만들어 반환한다. 아까와는 다르게 ServletInvocableHandlerMethod의 예외를 처리할 빈에는 컨트롤러가 아닌 ControllerAdvice 빈이 존재한다.
@Nullable
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
@Nullable HandlerMethod handlerMethod, Exception exception) {
...
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
ControllerAdviceBean advice = entry.getKey();
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method, this.applicationContext);
}
}
}
return null;
}
5. ControllerAdvice의 ExceptionHandler 메소드를 invoke하여 예외를 반환함
반환받은 ServletInvocableHandlerMethod에는 ExceptionHandler를 갖는 빈과 ExceptionHandler의 구현 메소드가 존재한다. 스프링은 리플렉션 API를 이용해 ExceptionHandler의 구현 메소드를 호출해 처리한 에러를 반환한다.
@Nullable
protected Object doInvoke(Object... args) throws Exception {
Method method = getBridgedMethod();
try {
if (KotlinDetector.isSuspendingFunction(method)) {
return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args);
}
return method.invoke(getBean(), args);
}
...
}
[ ControllerAdvice는 AOP로 구현되어 있을까? ]
회사의 면접을 보고 온 친구가 ControllerAdvice는 어떻게 구현되었냐는 질문을 받았다. 친구가 잘 모르겠다고 하자 면접관은 AOP라고 설명을 해주었다. 그리고 위와 해외 포스팅을 보면 실제로 ControllerAdvice가 AOP로 구현되어 있음과 예외 처리를 공통의 부가 기능으로 제공한다는 점에서 AOP의 Advice로부터 이름을 따왔음을 알 수 있다.
누군가는 JDK 동적 프록시나 CGLib 등을 이용해 프록시를 적용하지 않았기 때문에 AOP가 아니라고 얘기할 수 있다. 하지만 프록시 등을 사용하는 것은 결국 AOP라는 개념을 어떻게 구현할 것인지에 대한 기술적 요소이다. 따라서 AOP가 아니라기 보다는 구현된 방식이 프록시를 활용하지 않았다고 이해하는 것이 더욱 바람직할 것이다.