ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Controller Advice: Global vs. Custom Advice
    Spring 2025. 3. 20. 11:00
    728x90
    반응형

    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
    반응형
Designed by Tistory.