HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
✍🏻
Learnary (learn - diary)
/
[AOP] - ControllerAdvice 동작원리

[AOP] - ControllerAdvice 동작원리

📚 목차
🧩 @ExceptionHandler🔖 @ControllerAdvice👀 @RestControllerAdvice🏃 @ControllerAdvice 실행 순서🤔 의문사항⚡️ 참고 사이트

🧩 @ExceptionHandler

@Controller, @RestController가 적용된 Bean에서 발생된 예외를 잡아서 하나의 메서드에서 처리해주는 기능입니다.
(@Service나 @Repository 등 다른 Bean에서는 사용할 수 없습니다.)
value 설정을 통해서 어떤 예외를 처리할지 설정할 수 있습니다. (주의사항은 value를 지정하지 않으면 모든 예외를 처리하기 때문에 설정을 해주어야 합니다!)
와 같이 2개 이상도 등록 가능합니다.
 
하지만 이를 @Controller단에 정의하게 된다면, 다른 Controller의 예외는 처리할 수 없으므로 각각의 Controller에 정의해줘야 합니다.
그렇게 되면 같은 예외처리임에도 불구하고 각각의 Controller의 정의해줘야 하는 코드의 중복이 발생하게 됩니다.
이를 한번에 처리할 수 있게 해주는 것이 @ControllerAdvice 입니다.

🔖 @ControllerAdvice

@Controller 애노테이션이 있는 모든 곳에서의 예외를 잡을 수 있도록 해줍니다.
@ControllerAdvice 안에 있는 @ExceptionHandler는 모든 컨트롤러에서 발생하는 예외상황을 잡을 수 있습니다.
 
속성 설정을 통해서 원하는 컨트롤러나 패키지만 선택 할 수 있고, 선택하지 않는다면 모든 패키지에 있는 컨트롤러를 담당하게 됩니다.

👀 @RestControllerAdvice

| @ControllerAdvice + @ResponseBody → @RestControllerAdvice

🏃 @ControllerAdvice 실행 순서

notion image
  1. Controller단에서 오류가 발생하도록 Request 요청했습니다.
  1. DispatcherServlet → 서블릿에서 요청을 처리합니다.
    1. 2-1. doDispatch() 에서 Controller 실행 → 오류 발생 → processDispatchResult()
      2-2. processDispatchResult() : 오류가 있다면 processHandlerException() 구문을 실행!
      2-3. processHandlerException() 에서 실제로 오류를 처리합니다.
      • var6 에는 총 2개의 객체가 있습니다.
        • notion image
        • DefaultErrorAttributes
        • HandlerExceptionResolverComposite
  1. HandlerExceptionResolverComposite 이 클래스에서 처리를 하게 됩니다.
      • var5 에는 총 3개의 객체가 있습니다.
        • notion image
        • ExceptionHandlerExceptionResolver
        • ResponseStatusExceptionResolver
        • DefaultHandlerExceptionResolver
  1. ExceptionHandlerExceptionResolver 에서 실행할 핸들러를 찾아서 오류를 처리하게 됩니다.
    1. 1-1. 결과적으로 여기서 1) ControllerAdvice를 찾아서 2) invoke를 통해 오류를 처리합니다.
      ↑
      ✨ getExceptionHandlerMethod() → ControllerAdivce 객체를 찾는 메서드 입니다.
    2. 해당 Controller에 ExceptionHandler 정의 된것과 매칭된다면 그것을 실행하게 됩니다!
    3. ControllerAdvice를 지정한 클래스 빈을 찾아서 실행하게 됩니다.
      1. (저는 GeneralExceptionHandler 클래스 정의 → generalExceptionHandler가 보이시죠?)
        notion image
        notion image

🤔 의문사항

RestControllerAdvice도 똑같은가?
notion image
변경을 하고 진행을 해보았습니다.
notion image
✨ 똑같은 ControllerAdviceBean으로 등록되어 사용되는걸 확인할 수 있었습니다.
 
Controller 자체에 ExceptionHandler가 있을 경우, 우선 처리가 되는가? → 어디서? 👀
notion image
ExceptionHandlerExceptionResolver - getExceptionHandlerMethod() 에서 Ealry return!
 
ControllerAdvice - package를 지정한다면 우선적으로 실행 되는가?
패키지를 지정하여 임시 ControllerAdvice 클래스를 지정해보았습니다.
그럼 GeneralExceptionHandler vs TestExceptionHandler 무엇이 실행 될까요?
 
순서는 그대로 였고, 처리도 GeneralExceptionHandler 여기서 처리 되었습니다.
notion image
✨ basePackages 설정이 우선순위와는 상관이 없다는것을 알게되었습니다.
 
그러다가 블로그를 하나를 발견하게 됩니다. @Order / @Priority 를 이용해서 우선순위를 지정할 수 있다는 글이었습니다.
@ControllerAdvice 알아보기
를 통해서 어플리케이션 전역적으로 exception을 핸들링 할 수 있다. 다르게 표현하면 메서드에서 던져지는 exceptions들의 interceptor라고 할 수 있다. (shared across multiple @Controller classes) 주로 에서 전역적으로 처리하고 싶은 어노테이션은 , , 등이 있다. 클래스가 어노테이션에서 전역적인 exception handling 을 구현할 수 있도록 하는 base class이다.
@ControllerAdvice 알아보기
https://yjksw.github.io/springboot-controller-advice/
@ControllerAdvice 알아보기
위의 블로그 글처럼 @Order 를 설정해보니...
notion image
notion image
순서가 바뀌는 것을 확인할 수 있었습니다. 😳

⚡️ 참고 사이트

ExceptionHandler 와 ControllerAdvice
@ExceptionHandler 는 @Controller , @RestController 가 적용된 Bean 에서 발생하는 예외를 잡아서 하나의 메서드에서 처리해주는 기능이다. @ExceptionHandler 에 설정한 예외가 발생하면 handler가 실행된다. @Controller, @RestController가 아닌 @Service 나 @Repository 가 적용된 Bean에서는 사용할 수 없다. @ExceptionHandler 인터페이스로 들어가 보면 아래와 같다. value 설정을 통하여 어떤 예외를 잡을지 설정할 수 있다.
ExceptionHandler 와 ControllerAdvice
https://tecoble.techcourse.co.kr/post/2021-05-10-controller_advice_exception_handler/
ExceptionHandler 와 ControllerAdvice
[Spring] ControllerAdvice는 AOP로 구현되어 있을까? ControllerAdvice의 동작 과정 소스 코드로 살펴보기
이번에는 ControllerAdvice의 동작 과정이 어떻게 되는지 코드로 직접 살펴보도록 하겠습니다. 디스패처 서블릿이 에러를 catch함 해당 에러를 처리할 수 있는 처리기(HandlerExceptionResolver)가 에러를 처리함 컨트롤러의 ExceptionHandler로 처리가능한지 검사함 ControllerAdvice의 ExceptionHandler로 처리가능한지 검사함 ControllerAdvice의 ExceptionHandler 메소드를 invoke하여 예외를 반환함 1. 디스패처 서블릿이 에러를 catch함 스프링에서 모든 요청을 가장 먼저 받는 곳은 디스패처 서블릿이다.
[Spring] ControllerAdvice는 AOP로 구현되어 있을까? ControllerAdvice의 동작 과정 소스 코드로 살펴보기
https://mangkyu.tistory.com/246
[Spring] ControllerAdvice는 AOP로 구현되어 있을까? ControllerAdvice의 동작 과정 소스 코드로 살펴보기
progress
Done
Tags
Spring
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ExceptionHandler { Class<? extends Throwable>[] value() default {}; }
@ExceptionHandler({Exception.class, RuntimeException.class})
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface ControllerAdvice { @AliasFor("basePackages") String[] value() default {}; @AliasFor("value") String[] basePackages() default {}; Class<?>[] basePackageClasses() default {}; Class<?>[] assignableTypes() default {}; Class<? extends Annotation>[] annotations() default {}; }
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @ControllerAdvice @ResponseBody public @interface RestControllerAdvice { @AliasFor( annotation = ControllerAdvice.class ) String[] value() default {}; @AliasFor( annotation = ControllerAdvice.class ) String[] basePackages() default {}; @AliasFor( annotation = ControllerAdvice.class ) Class<?>[] basePackageClasses() default {}; @AliasFor( annotation = ControllerAdvice.class ) Class<?>[] assignableTypes() default {}; @AliasFor( annotation = ControllerAdvice.class ) Class<? extends Annotation>[] annotations() default {}; }
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ... HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); ... // ✨ 1. 실제로 여기서 컨트롤러단으로 넘어가 처리를 하게 됩니다. // → 하지만 오류가 발생 하겠죠? mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); ... } catch (Exception var20) { // ✨ 2. Catch 부분에서 해당 오류를 세팅합니다. dispatchException = var20; } catch (Throwable var21) { dispatchException = new NestedServletException("Handler dispatch failed", var21); } // ✨ 3. 여기서 오류를 가지고 처리를 하게 됩니다. this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException); ... }
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; // ✨ exception이 1-1 메서드에서 넘어 왔기 때문에, 이 구문을 진행하게 됩니다. if (exception != null) { if (exception instanceof ModelAndViewDefiningException) { this.logger.debug("ModelAndViewDefiningException encountered", exception); mv = ((ModelAndViewDefiningException)exception).getModelAndView(); } else { // ✨ 이 구문을 진행하게 됩니다. → processHandlerException() Object handler = mappedHandler != null ? mappedHandler.getHandler() : null; mv = this.processHandlerException(request, response, handler, exception); errorView = mv != null; } } ... }
@Nullable protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception { request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); ModelAndView exMv = null; if (this.handlerExceptionResolvers != null) { Iterator var6 = this.handlerExceptionResolvers.iterator(); while(var6.hasNext()) { HandlerExceptionResolver resolver = (HandlerExceptionResolver)var6.next(); // ✨ 이 구문을 진행하게 됩니다. exMv = resolver.resolveException(request, response, handler, ex); if (exMv != null) { break; } } } ... }
@Nullable protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) { // ✨ 1. getExceptionHandlerMethod() 메서드를 통해서 어디서 처리를 할건지 찾아냅니다. // (ControllerAdvice 클래스를 생성했다면, 그 설정된 빈을 가져오게 되겠죠?) ServletInvocableHandlerMethod exceptionHandlerMethod = this.getExceptionHandlerMethod(handlerMethod, exception); ... // ✨ 2. invoke를 통해서 이제 ControllerAdvice로 가서 처리를 하게 됩니다. exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments); ... }
@Nullable protected ServletInvocableHandlerMethod getExceptionHandlerMethod(@Nullable HandlerMethod handlerMethod, Exception exception) { Class<?> handlerType = null; if (handlerMethod != null) { // ✨ 1. 해당 Controller ExceptionHandler가 있다면 이 구문에서 처리된다. 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) { // ✨ <1> 에 해당한다면 여기서 Ealry Return! return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method, this.applicationContext); } if (Proxy.isProxyClass(handlerType)) { handlerType = AopUtils.getTargetClass(handlerMethod.getBean()); } } // ✨ 2. 여기서 ControllerAdvice를 찾게 됩니다. Iterator var9 = this.exceptionHandlerAdviceCache.entrySet().iterator(); while(var9.hasNext()) { Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry = (Map.Entry)var9.next(); 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; }
@Slf4j @ControllerAdvice(basePackages = "com.midas.outflearn.controller.lecture") public class TestExceptionHandler { ... }