ControllerAdvice는 메소드 인자를 어떻게 가지고올까

chloe·2025년 2월 15일
0

자프링 더 알아가기

목록 보기
11/11

찾아보게 된 계기

기존에는 @ExceptionHandler 을 붙인 메소드에서 해당하는 Exception만 메소드의 인자로 전달받아 사용했으나,
보상 트랜젝션을 수행을 검토하면서, Exception 외의 받을 수 있는 argument는 어떤게 있고, argument에 값이 전달되는 원리가 궁금해 찾아보게되었다.

결론

@ExceptionHandler 메소드에 올 수 있는 Parameter

대표적으론 다음과 같다.

  • HttpServletRequest (ServletRequest, InputStream, Reader 등도 가능)
  • HttpServletResponse
  • HttpSession
  • HttpMethod
  • Locale
  • Principal
  • Model
  • RedirectAttributes

그래서 아래와 ExceptionHandler method를 선언하면 실행 시, 알아서 값이 들어온다.

  @ExceptionHandler(Exception.class)
  public CommonResponse handleException(Exception e, HttpServletRequest request,
      HttpServletResponse response, HttpMethod method) {
    log.error(e.getMessage(), e);
    return createErrorResponse(ERR_UNEXPECTED);
  }

만약 지원하지 않는 타입이 들어오면 아래와 같은 에러가 발생한다.

2025-02-16 02:01:54.806  WARN 55113 --- [nio-8081-exec-1] .m.m.a.ExceptionHandlerExceptionResolver : Failure in @ExceptionHandler com.controller.ControllerAdvice#handleException(Exception, HttpServletRequest, HttpServletResponse, RedirectAttributes, HttpSession, Model, HttpMethod, Principal, String)

java.lang.IllegalStateException: Could not resolve parameter [8] in public com.CommonResponse<java.lang.Object> com.controller.ControllerAdvice.handleException(java.lang.Exception,javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse,org.springframework.web.servlet.mvc.support.RedirectAttributes,javax.servlet.http.HttpSession,org.springframework.ui.Model,org.springframework.http.HttpMethod,java.security.Principal,java.lang.String): 
	No suitable resolver at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:176)
	

ExceptionHandlerExceptionResolver.getDefaultArgumentResolvers() 반환 값의 Resolver들이 지원하는 타입이 들어올 수 있다고 보면 된다 (아래 코드 참조. Custom도 가능)

ExceptionHandler가 실행되는 과정

  1. ExceptionHandlerExceptionResolver가 생성될 때, @ControllerAdvice가 붙은 빈을 검색하고, 각 빈에서 @ExceptionHandler가 붙은 메서드를 찾아 저장한다.
  2. 이후,getDefaultArgumentResolvers() 에서 Method에 argument를 처리해주는 HandlerMethodArgumentResolver 들을 지정한다.
  3. 예외가 발생하면 DispatcherServlet이 적절한 @ExceptionHandler 메서드를 탐색한다.
  4. ExceptionHandlerMethodResolver가 해당 예외를 처리할 수 있는 메서드를 찾아 ServletInvocableHandlerMethod로 래핑한다.
  5. ServletInvocableHandlerMethodInvocableHandlerMethod.invokeForRequest()를 호출하여 HandlerMethodArgumentResolverComposite을 통해 메서드 인자로 넣을 값들을 찾은 후, method.invoke(bean, args)를 실행하여 @ExceptionHandler 메서드를 호출한다.

Argument에 값이 전달되는 원리

  • 위 2번의 HandlerMethodArgumentResolver 들이 각각 지원하는 Type을 resolveArgument 메소드로 받은 argument들을 사용해 만들어낸다.
  • 그리고 Reflection으로 ExceptionHandler 메소드를 동적으로 실행 (Method.invoke())

Diagram

Class Diagram

Sequence Diagram

Code

DispatcherServlet에서 부터 @ExceptionHandler가 붙은 method를 실행하기까지 필요한 코드들만 간추렸다.
주석의 번호가 적힌 함수들을 위주로 따라가면 된다.

public class DispatcherServlet {
  protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

      try {
        try {
          ModelAndView mv = null;
          Exception dispatchException = null;

          try {
            // ...
            HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
            
            // 1. handler 호출
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            this.applyDefaultViewName(processedRequest, mv);
            mappedHandler.applyPostHandle(processedRequest, response, mv);
          } catch (Exception ex) {
            // 2. 에러 저장
            dispatchException = ex;
          } catch (Throwable err) {
            dispatchException = new NestedServletException("Handler dispatch failed", err);
          }
          // 3. result 수행
          this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        } 


    }
  }
    
  private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
    boolean errorView = false;
    if (exception != null) {
      Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
      // exception 처리
      mv = this.processHandlerException(request, response, handler, exception);
      errorView = mv != null;
    }

  }

  protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception {
    if (this.handlerExceptionResolvers != null) {
        // HandlerExceptionResolver를 구현하는 빈들을 초기화 시 저장 (ExceptionHandlerExceptionResolver도 그 중 하나)
      for(HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
        exMv = resolver.resolveException(request, response, handler, ex);
      }
    }

  }
}

public abstract class AbstractHandlerMethodExceptionResolver extends AbstractHandlerExceptionResolver {

  @Nullable
  protected final ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
    HandlerMethod handlerMethod = handler instanceof HandlerMethod ? (HandlerMethod)handler : null;
    return this.doResolveHandlerMethodException(request, response, handlerMethod, ex);
  }

  @Nullable
  protected abstract ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception ex);
}

// ControllerAdvice들을 찾아 가지고 있음
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver implements ApplicationContextAware, InitializingBean {

  protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
    // 4. getExceptionHandlerMethod는 exception과 매칭되는 ExceptionHandler가 붙은 Method를 찾아 ServletInvocableHandlerMethod에 넣음
    ServletInvocableHandlerMethod exceptionHandlerMethod = this.getExceptionHandlerMethod(handlerMethod, exception);
    if (this.argumentResolvers != null) {
        // 0-1의 resolver들을 넣은 composite 객체를 넣음
        exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
    }
    

    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    ModelAndViewContainer mavContainer = new ModelAndViewContainer();
    ArrayList<Throwable> exceptions = new ArrayList();

    // 5. exception 객체들은 인자의 앞에 순서대로 넣음
    Object[] arguments = new Object[exceptions.size() + 1];
    exceptions.toArray(arguments);
    arguments[arguments.length - 1] = handlerMethod;
    // 6. exception을 처리하는 메소드를 실행
    exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments);

    
  }

  @Override
	public void afterPropertiesSet() {
		// 0. ControllerAdvice를 전부 scan
		initExceptionHandlerAdviceCache();

		if (this.argumentResolvers == null) {
            // 0-1. Method의 Argument들을 넣을 수 있도록 해주는 Resolver들을 초기에 지정
			List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
			this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
		}
	}

  protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
		List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();

		// Annotation-based argument resolution
		resolvers.add(new SessionAttributeMethodArgumentResolver());
		resolvers.add(new RequestAttributeMethodArgumentResolver());

		// Type-based argument resolution
		resolvers.add(new ServletRequestMethodArgumentResolver());
		resolvers.add(new ServletResponseMethodArgumentResolver());
		resolvers.add(new RedirectAttributesMethodArgumentResolver());
		resolvers.add(new ModelMethodProcessor());

		// Custom arguments
		if (getCustomArgumentResolvers() != null) {
			resolvers.addAll(getCustomArgumentResolvers());
		}

		// Catch-all
		resolvers.add(new PrincipalMethodArgumentResolver());

		return resolvers;
	}

  protected ServletInvocableHandlerMethod getExceptionHandlerMethod(@Nullable HandlerMethod handlerMethod, Exception exception) {

		Class<?> handlerType = null;

		for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
			ControllerAdviceBean advice = entry.getKey();
			if (advice.isApplicableToBeanType(handlerType)) {
				ExceptionHandlerMethodResolver resolver = entry.getValue();
        // 해당 exception을 처리하는 method를 전달
				Method method = resolver.resolveMethod(exception);
				if (method != null) {
					return new ServletInvocableHandlerMethod(advice.resolveBean(), method, this.applicationContext);
				}
			}
		}

		return null;
	}

  //ExceptionHandlerExceptionResolver 생성 시 설정
  private void initExceptionHandlerAdviceCache() {

    // ControllerAdvice bean을 찾아 adviceBeans 반환
		List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
		for (ControllerAdviceBean adviceBean : adviceBeans) {
			Class<?> beanType = adviceBean.getBeanType();
      // @ExceptionHandler 를 가진 method들을 가지고 있음
			ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
			if (resolver.hasExceptionMappings()) {
				this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
			}
		}
	}
}

public class ControllerAdviceBean implements Ordered {
  public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {

		List<ControllerAdviceBean> adviceBeans = new ArrayList<>();
		for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, Object.class)) {
			if (!ScopedProxyUtils.isScopedTarget(name)) {
				ControllerAdvice controllerAdvice = beanFactory.findAnnotationOnBean(name, ControllerAdvice.class);
				if (controllerAdvice != null) {
					adviceBeans.add(new ControllerAdviceBean(name, beanFactory, controllerAdvice));
				}
			}
		}
		OrderComparator.sort(adviceBeans);
		return adviceBeans;
	}

}

public class ExceptionHandlerMethodResolver {
  public static final MethodFilter EXCEPTION_HANDLER_METHODS = method ->
			AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);

  public ExceptionHandlerMethodResolver(Class<?> handlerType) {
		for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
			for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
				addExceptionMapping(exceptionType, method);
			}
		}
	}
}


public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
  public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
    Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
  }

}


public class InvocableHandlerMethod extends HandlerMethod {

  private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();

  public void setHandlerMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) {
		this.resolvers = argumentResolvers;
	}

	public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {

        // 7. 파라미터로 넣을 값들을 찾음
		Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
        // 8. 인자값으로 메소드 호출 및 return 값 전달
		return doInvoke(args);
	}


  protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {

		MethodParameter[] parameters = getMethodParameters();
		if (ObjectUtils.isEmpty(parameters)) {
			return EMPTY_ARGS;
		}

		Object[] args = new Object[parameters.length];
		for (int i = 0; i < parameters.length; i++) {
			MethodParameter parameter = parameters[i];
			parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
			args[i] = findProvidedArgument(parameter, providedArgs);

			try {
                // 7-1. 파라미터 별로 맞는 인자를 찾기위해 호출
				args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
			}

		}
		return args;
	}

  protected Object doInvoke(Object... args) throws Exception {
		Method method = getBridgedMethod();
		try {
			return method.invoke(getBean(), args);
		}
  }
}

public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {

  public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        // 7-2. parameter에 맞는 resolver를 찾음 (0-1. 의 List 중에서 해당 paramter를 지원하는 resolver)
		HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
        // 7-3. paramter에 맞는 인자를 리턴
		return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
	}

  private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
		HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
		if (result == null) {
			for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
				if (resolver.supportsParameter(parameter)) {
					result = resolver;
					this.argumentResolverCache.put(parameter, result);
					break;
				}
			}
		}
		return result;
	}

}

public interface HandlerMethodArgumentResolver {

	/**
	 * Whether the given {@linkplain MethodParameter method parameter} is
	 * supported by this resolver.
	 */
	boolean supportsParameter(MethodParameter parameter);

	/**
	 * Resolves a method parameter into an argument value from a given request.
	 */
	Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

ExceptionHandlerExceptionResolver.. ExceptionHandlerMethodResolver... 이름이 다들 비슷해서 코드 따라가는데 시간이 한참 걸렸지만 재미있었다.

profile
삽질전문 아티스트

0개의 댓글

관련 채용 정보