-
Spring Controller Advice: Global vs. Custom AdviceSpring 2025. 3. 20. 11:00728x90반응형
Spring의 @ControllerAdvice는 애플리케이션 전역에서 발생하는 예외를 처리하는 강력한 도구이며 특정 컨트롤러나 패키지에 국한되지 않고 다양한 방식으로 활용할 수 있습니다.
자사 API 코드를 분석하던 중 Custom @ControllerAdvice에 MissingServletRequestParameterException 예외를 처리하도록 정의되어 있는 것을 확인했습니다. 하지만 이 예외는 Custom @ControllerAdvice에서 절대 처리되지 않는다는 사실을 알고 있었기 때문에 이를 Global @ControllerAdvice에서 정의해야 한다는 의견을 제안했습니다. 이 기회에 Custom @ControllerAdvice가 왜 MissingServletRequestParameterException과 같은 예외를 처리할 수 없는지에 대해 간단히 정리해보려 합니다.
Global Adivce vs Custom Advice
- Global Advice : base packages를 지정하지 않은 경우, 모든 컨트롤러에서 발생하는 예외를 처리합니다.
- Custom Advice : base packages를 지정한 경우 해당 패키지 내 컨트롤러에서 발생하는 예외를 처리합니다.
Why? Gloabl Advice 작동할까?
Spring의 예외 처리 메커니즘은 ExceptionHandlerExceptionResolver 클래스에서 구현됩니다.
이 클래스의 내부 동작을 살펴보면 Global @ControllerAdvice가 모든 예외를 처리할 수 있는 이유를 알 수 있습니다.
아래는 핵심 메서드와 그 역할을 간략히 정리한 내용입니다.
1. doResolveHandlerMethodException
// ExceptionHandlerExceptionResolver.class @Nullable protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) { ServletInvocableHandlerMethod exceptionHandlerMethod = this.getExceptionHandlerMethod(handlerMethod, exception); if (exceptionHandlerMethod == null) { return null; } else { if (this.argumentResolvers != null) { exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) { exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } ServletWebRequest webRequest = new ServletWebRequest(request, response); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); ArrayList<Throwable> exceptions = new ArrayList<>(); try { if (this.logger.isDebugEnabled()) { this.logger.debug("Using @ExceptionHandler " + exceptionHandlerMethod); } Throwable cause; for (Throwable exToExpose = exception; exToExpose != null; exToExpose = cause != exToExpose ? cause : null) { exceptions.add(exToExpose); cause = exToExpose.getCause(); } Object[] arguments = new Object[exceptions.size() + 1]; exceptions.toArray(arguments); arguments[arguments.length - 1] = handlerMethod; exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments); } catch (Throwable invocationEx) { if (!exceptions.contains(invocationEx) && this.logger.isWarnEnabled()) { this.logger.warn("Failure in @ExceptionHandler " + exceptionHandlerMethod, invocationEx); } return null; } if (mavContainer.isRequestHandled()) { return new ModelAndView(); } else { ModelMap model = mavContainer.getModel(); HttpStatus status = mavContainer.getStatus(); ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status); mav.setViewName(mavContainer.getViewName()); if (!mavContainer.isViewReference()) { mav.setView((View) mavContainer.getView()); } if (model instanceof RedirectAttributes) { Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes(); RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); } return mav; } } }
- 해당 메서드는 발생한 예외를 처리할 수 있는 @ExceptionHandler 메서드를 찾아 호출합니다.
- getExceptionHandlerMethod를 통해 적절한 Handler를 찾고 이를 실행한 뒤 결과를 반환합니다.
2. getExceptionHandlerMethod
// ExceptionHandlerExceptionResolver.class @Nullable protected ServletInvocableHandlerMethod getExceptionHandlerMethod(@Nullable HandlerMethod handlerMethod, Exception exception) { Class<?> handlerType = null; if (handlerMethod != null) { handlerType = handlerMethod.getBeanType(); ExceptionHandlerMethodResolver resolver = (ExceptionHandlerMethodResolver) 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); } if (Proxy.isProxyClass(handlerType)) { handlerType = AopUtils.getTargetClass(handlerMethod.getBean()); } } for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) { ControllerAdviceBean advice = (ControllerAdviceBean) entry.getKey(); if (advice.isApplicableToBeanType(handlerType)) { ExceptionHandlerMethodResolver resolver = (ExceptionHandlerMethodResolver) entry.getValue(); Method method = resolver.resolveMethod(exception); if (method != null) { return new ServletInvocableHandlerMethod(advice.resolveBean(), method, this.applicationContext); } } } return null; }
- 해당 메서드는 @ControllerAdvice에 정의된 예외 처리 메서드 중 적합한 것을 찾습니다.
- isApplicableToBeanType(handlerType)에서 basePackages 조건을 확인합니다.
- Global Advice는 이 조건이 항상 true로 평가됩니다.
3. test
// HandlerTypePredicate.class public boolean test(@Nullable Class<?> controllerType) { if (!this.hasSelectors()) { return true; // basePackages가 없으면 모든 컨트롤러에 적용 } else { if (controllerType != null) { for (String basePackage : this.basePackages) { if (controllerType.getName().startsWith(basePackage)) { return true; } } for (Class<?> clazz : this.assignableTypes) { if (ClassUtils.isAssignable(clazz, controllerType)) { return true; } } for (Class<? extends Annotation> annotationClass : this.annotations) { if (AnnotationUtils.findAnnotation(controllerType, annotationClass) != null) { return true; } } } return false; } }
- basePackages가 지정되지 않은 경우(Global Advice) 모든 컨트롤러에 대해 true를 반환합니다.
- Custom Advice는 basePackages에 해당하는 컨트롤러만 필터링합니다.
4. resolveMethodByThrowable
@Nullable public Method resolveMethodByThrowable(Throwable exception) { Method method = this.resolveMethodByExceptionType(exception.getClass()); if (method == null) { Throwable cause = exception.getCause(); if (cause != null) { method = this.resolveMethodByThrowable(cause); } } return method; }
- 해당 메서드는 Handler가 발생한 예외를 처리하도록 정의되어 있는지 확인합니다.
5. 결론
MissingServletRequestParameterException은 Spring MVC의 요청 파라미터 검증 중 DispatcherServlet에서 발생하며 컨트롤러 메서드 호출 전에 처리됩니다.
Custom @ControllerAdvice는 basePackages로 제한된 컨트롤러의 예외만 다룰 수 있기 때문에 이 예외가 범위 밖이거나 컨트롤러 실행 전 발생 시 처리할 수 없습니다.
하지만 Global @ControllerAdvice는 범위 제한 없이 모든 예외를 포괄하므로 이를 효과적으로 처리할 수 있습니다.
728x90반응형'Spring' 카테고리의 다른 글
[Rest Docs]Test 없는 API 문서는 시한폭탄 (0) 2025.02.26 Spring Cloud Eureka와 Go의 만남 (1) 2025.01.06 [레거시 리팩토링] 상속을 넘어 컴포지션으로 가보자 (0) 2024.12.17 DTO(Data Transfer Object)를 쓰는 이유? (1) 2024.11.11 BeanPostProcessor 사용법: 빈 초기화 과정에서의 조작과 시점 이해하기 (0) 2024.11.07