오늘의하루

Spring Controller Advice: Global vs. Custom Advice 본문

Spring

Spring Controller Advice: Global vs. Custom Advice

오늘의하루_master 2025. 3. 20. 11:00
반응형

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 범위 제한 없이 모든 예외를 포괄하므로 이를 효과적으로 처리할 있습니다.

반응형
Comments