Spring Boot에서 @RestControllerAdvice 우선순위 문제 해결하기 (aka @Order)

Denia·2024년 3월 18일
1

TroubleShooting

목록 보기
23/25
post-custom-banner

결론만 궁금하신 분들은 바로 결론으로 가시면 됩니다!

문제 인식

프로젝트의 vue쪽 코드를 수정하고 수정된 코드가 잘 반영되었는지, 배포 서버에서 테스트를 해보다가 RestControllerAdvice가 이상하게 동작하는 것을 발견했다.

어떻게 이상하게 동작 했는지를 설명하기 위해서는, 지금 내 프로젝트가 어떻게 구성되어 있는지를 간단하게 설명을 해야 한다.

현재 내 프로젝트는 RestControllerAdvice를 2개 사용하고 있다.

  1. ApiControllerAdvice

    • 자주 발생하는 일반적인 예외들을 처리한다.
      [ Ex. BindException, EntityNotFoundException, IllegalArgumentException, BusinessException (내가 직접 만든 Custom Exception) ]
    • 예외가 발생하면 log를 남기고, 클라이언트한테 해당 예외에 맞는 상태 코드와 예외 메세지를 전달한다.
    
    @RestControllerAdvice
    @Slf4j
    public class ApiControllerAdvice {
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ExceptionHandler(BindException.class)
        public ApiResponse<Object> bindException(final BindException e) {
            final String errorMessage = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
            log.warn("API Controller BindException : {}", errorMessage);
    
    				return ApiResponse.of(HttpStatus.BAD_REQUEST, errorMessage, null);
        }
    
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ExceptionHandler(EntityNotFoundException.class)
        public ApiResponse<Object> entityNotFoundException(final EntityNotFoundException e) {
            final String errorMessage = e.getMessage();
            log.warn("API Controller EntityNotFoundException : {}", errorMessage);
    
    				return ApiResponse.of(HttpStatus.BAD_REQUEST, errorMessage, null);
        }
    
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ExceptionHandler(IllegalArgumentException.class)
        public ApiResponse<Object> illegalArgumentException(final IllegalArgumentException e) {
            final String errorMessage = e.getMessage();
            log.warn("API Controller IllegalArgumentException : {}", errorMessage);
    
            return ApiResponse.of(HttpStatus.BAD_REQUEST, errorMessage, null);
        }
    
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ExceptionHandler(BusinessException.class)
        public ApiResponse<Object> businessException(final BusinessException e) {
            final String errorMessage = e.getMessage();
            log.warn("API Controller BusinessException : {}", errorMessage);
    				
    				return ApiResponse.of(HttpStatus.BAD_REQUEST, errorMessage, null);
        }
    }
  2. ServerErrorDetectControllerAdvice

    • 내가 예상하지 못한 Exception이 발생할 경우 처리한다. (ApiControllerAdvice 에서 처리 못하는 예외들)
    • 예외가 발생하면 log를 남기고, Slack을 통해 예외 내용을 전달한다.
      • 내가 예상하지 못한 예외들은 큰 문제로 이어질 수 있기 때문에 빠르게 대응하기 위해 Slack이랑 연동을 시켜뒀다.
    • 로컬에서는 사용할 일이 없기 때문에, prod 환경에서만 실행이 되도록 @Profile 설정을 했다.
    
    @Profile("prod")
    @RequiredArgsConstructor
    @RestControllerAdvice
    @Slf4j
    public class ServerErrorDetectControllerAdvice {
        private final SlackApi slackApi;
    
        @ExceptionHandler(Exception.class)
        public void handleException(final HttpServletRequest req, final Exception e) throws Exception {
            log.error("Server Error : {}", e.getMessage(), e);
    
            slackApi.call(buildSlackMessage(buildSlackAttachment(req, e)));
            throw e;
        }
        
        //... 중략
    }
    

내가 지금까지 스프링을 공부하면서 스프링에 대해서 깨달은 점은, 스프링은 항상 구체적인 조건들을 먼저 탐색하고 처리했었다.

그래서 이 경험을 기준으로, 여러 개의 RestControllerAdvice가 있어도 당연히 구체적인 예외를 지정한 RestControllerAdvice에서 예외를 먼저 처리할 것이라고 생각했다.

내가 생각한 예외 처리 로직 순서는 다음과 같았다.

  1. 특정 예외가 발생

  2. ApiControllerAdvice를 먼저 탐색 → 해당 RestControllerAdvice에서 처리하지 못하는 경우, 다음 RestControllerAdvice에서 예외 처리를 하도록 예외를 넘긴다.

  3. ServerErrorDetectControllerAdvice를 탐색 → 해당 RestControllerAdvice는 모든 Exception을 처리하므로 error 로그를 남기고 Slack으로 메세지를 보낸 후 예외를 throw하고 request를 종료한다.

그런데 실제로 서버가 예외를 처리하는 로직은 무조건적으로 모든 에러가 ServerErrorDetectControllerAdvice에서 처리가 되었고, 모든 예외가 Slack으로 전달이 되었다. 😵

ServerErrorDetectControllerAdvice

(※ 로그를 보면 IllegalArgumentException이 발생했는데도 ServerErrorDetectControllerAdvice가 처리하고 있다. 😭)

"Local에서 테스트 하지 않고 배포한건가?" 라고 생각이 드실수도 있는데, Local에서 테스트를 여러번 했었는데 매번 ApiControllerAdvice가 먼저 실행이 됐었습니다. ㅜㅜ


문제 진단 1

  1. 처음에 내가 생각하기로는 ApiControllerAdvice가 등록이 되지 않았고 그래서 ApiControllerAdvice가 실행이 되지 않은 줄 알았다.
    1. ApiControllerAdvice Bean이 생성될 때 log를 남기도록 코드를 수정 후 다시 배포하여 테스트를 진행
    2. ApiControllerAdvice는 정상적으로 생성이 되었지만 여전히 모든 예외는 ServerErrorDetectControllerAdvice가 처리
  2. 해당 문제에 대해서 검색을 해보니 다음 블로그 글을 찾을 수 있었고, 우선순위를 지정해주기로 했다. ( 블로그 글 : [spring] @ControllerAdvice 여러개 쓰기. )
    1. 당연히 @Order를 지정하지 않으면 기본 우선순위로 0이 잡힐 것이라고 생각했다.
      [ ※ @Order를 지정하지 않으면, 제일 낮은 우선순위로 잡히게 됩니다. (즉, 안 붙이는 거랑 @Order(Ordered.LOWEST_PRECEDENCE)를 붙이는 거랑 같다는 의미입니다.) ]
    2. ServerErrorDetectControllerAdvice 에만 @Order(Ordered.LOWEST_PRECEDENCE)를 붙여주면 문제가 해결될 것이라고 생각했다.
    3. 여전히 ServerErrorDetectControllerAdvice가 모든 예외를 처리
  3. Spring 코드를 까보면서 어떤게 문제인지 제대로 파악해보기로 했다.
    • 문제 진단 2에서 계속 진행하겠습니다.

문제 진단 2

내가 사용한 Order 어노테이션에 대해서 조금 더 알아보자.

  • @Order (Java Doc)
    • value의 기본 값은 Integer.MAX_VALUE이다. (Ordered.LOWEST_PRECEDENCE랑 같은 값이다.)

      @Retention(RetentionPolicy.RUNTIME)
      @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
      @Documented
      public @interface Order {
          int value() default Integer.MAX_VALUE;
      }
  • Ordered 인터페이스 (Java Doc)
    public interface Ordered {
        int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
        int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
    
        int getOrder();
    }

Spring에서 ControllerAdvice가 어떻게 등록되고 사용 되는지 알아보자.

ControllerAdvice 등록

  1. WebMvcConfigurationSupporthandlerExceptionResolver메서드가 실행된다.

    그리고 this.addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager); 메서드가 실행 된다.

    // WebMvcConfigurationSupport.class
    
    @Bean
    public HandlerExceptionResolver handlerExceptionResolver(@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
        List<HandlerExceptionResolver> exceptionResolvers = new ArrayList();
        this.configureHandlerExceptionResolvers(exceptionResolvers);
        if (exceptionResolvers.isEmpty()) {
    		    //해당 코드 실행됨
            this.addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
        }
    
        this.extendHandlerExceptionResolvers(exceptionResolvers);
        HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
        composite.setOrder(0);
        composite.setExceptionResolvers(exceptionResolvers);
        return composite;
    }
    
  2. addDefaultHandlerExceptionResolvers()메서드 안에서 exceptionHandlerResolverInit 된다.

    그 후 exceptionHandlerResolver.afterPropertiesSet()에서 exceptionHandlerAdviceCacheControllerAdvice Bean들을 추가한다.

    [ ※ 나중에 예외가 발생하게 되면, exceptionHandlerAdviceCache의 요소들을 순회하면서 예외를 처리할 ControllerAdvice를 정하게 된다. ]

    // WebMvcConfigurationSupport.class
    
    protected final void addDefaultHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers, ContentNegotiationManager mvcContentNegotiationManager) {
        ExceptionHandlerExceptionResolver exceptionHandlerResolver = this.createExceptionHandlerExceptionResolver();
        exceptionHandlerResolver.setContentNegotiationManager(mvcContentNegotiationManager);
        exceptionHandlerResolver.setMessageConverters(this.getMessageConverters());
        exceptionHandlerResolver.setCustomArgumentResolvers(this.getArgumentResolvers());
        exceptionHandlerResolver.setCustomReturnValueHandlers(this.getReturnValueHandlers());
        if (jackson2Present) {
            exceptionHandlerResolver.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
        }
    
        if (this.applicationContext != null) {
            exceptionHandlerResolver.setApplicationContext(this.applicationContext);
        }
    		
    		//afterPropertiesSet() 안에서 exceptionHandlerAdviceCache에 ControllerAdvice Bean들을 추가한다.
        exceptionHandlerResolver.afterPropertiesSet();
        exceptionResolvers.add(exceptionHandlerResolver);
        ResponseStatusExceptionResolver responseStatusResolver = new ResponseStatusExceptionResolver();
        responseStatusResolver.setMessageSource(this.applicationContext);
        exceptionResolvers.add(responseStatusResolver);
        exceptionResolvers.add(new DefaultHandlerExceptionResolver());
    }
  3. ExceptionHandlerExceptionResolverafterPropertiesSet()내부 구현을 살펴 보자.

    this.initExceptionHandlerAdviceCache()를 통해서 exceptionHandlerAdviceCacheControllerAdvice Bean들을 추가한다.

    // ExceptionHandlerExceptionResolver.class
    
    public void afterPropertiesSet() {
    		//실제로 exceptionHandlerAdviceCache에 ControllerAdvice Bean들을 추가하는 메서드
        this.initExceptionHandlerAdviceCache();
        this.initMessageConverters();
        List handlers;
        if (this.argumentResolvers == null) {
            handlers = this.getDefaultArgumentResolvers();
            this.argumentResolvers = (new HandlerMethodArgumentResolverComposite()).addResolvers(handlers);
        }
    
        if (this.returnValueHandlers == null) {
            handlers = this.getDefaultReturnValueHandlers();
            this.returnValueHandlers = (new HandlerMethodReturnValueHandlerComposite()).addHandlers(handlers);
        }
    
    }
  4. ControllerAdviceBean.findAnnotatedBeans(this.getApplicationContext()) 메서드를 통해서 ControllerAdvice에 해당 하는 Bean을 모두 찾아 온다.

    // ExceptionHandlerExceptionResolver.class
    
    // ★ Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache = new LinkedHashMap();
    
    private void initExceptionHandlerAdviceCache() {
        if (this.getApplicationContext() != null) {
    		    // ControllerAdvice에 해당 하는 Bean을 모두 찾는다.
            List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(this.getApplicationContext());
            Iterator var2 = adviceBeans.iterator();
    
    				// ... 중략
            }
        }
    }
  5. ControllerAdviceBeanfindAnnotatedBeans 메서드는 ApplicationContext에서 ControllerAdvice Bean을 모두 찾은 후 Order에 따라 정렬을 진행한다. (OrderComparator.sort(adviceBeans))

    // ControllerAdviceBean.class
    
    public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
        ListableBeanFactory beanFactory = context;
        if (context instanceof ConfigurableApplicationContext cac) {
            beanFactory = cac.getBeanFactory();
        }
    
        List<ControllerAdviceBean> adviceBeans = new ArrayList();
        String[] var3 = BeanFactoryUtils.beanNamesForTypeIncludingAncestors((ListableBeanFactory)beanFactory, Object.class);
        int var4 = var3.length;
    
        for(int var5 = 0; var5 < var4; ++var5) {
            String name = var3[var5];
            if (!ScopedProxyUtils.isScopedTarget(name)) {
                ControllerAdvice controllerAdvice = (ControllerAdvice)((ListableBeanFactory)beanFactory).findAnnotationOnBean(name, ControllerAdvice.class);
                if (controllerAdvice != null) {
    		            // 해당 하는 Bean을 모두 adviceBeans에 추가한다.
                    adviceBeans.add(new ControllerAdviceBean(name, (BeanFactory)beanFactory, controllerAdvice));
                }
            }
        }
    		
    		// Order에 따라 정렬한다.
        OrderComparator.sort(adviceBeans);
        return adviceBeans;
    }

    PriorityOrdered가 달려있는 Bean이 없기 때문에 getOrder 메서드를 통해 값을 받아와서 순서 비교를 하게 된다.

    어떤 Bean에도 @Order를 지정해주지 않았다면 i1i2는 같은 값을 가지게 되고, i1이 무조건 우선순위를 가지게 된다. → 어떤 Bean이 우선 순위를 가질지 알 수 없다.

    // OrderComparator.class
    
    public int compare(@Nullable Object o1, @Nullable Object o2) {
        return this.doCompare(o1, o2, (OrderSourceProvider)null);
    }
    
    private int doCompare(@Nullable Object o1, @Nullable Object o2, @Nullable OrderSourceProvider sourceProvider) {
        boolean p1 = o1 instanceof PriorityOrdered;
        boolean p2 = o2 instanceof PriorityOrdered;
        if (p1 && !p2) {
            return -1;
        } else if (p2 && !p1) {
            return 1;
        } else {
    		    //
            int i1 = this.getOrder(o1, sourceProvider);
            int i2 = this.getOrder(o2, sourceProvider);
            return Integer.compare(i1, i2);
        }
    }

    Spring Java Doc OrderComparator항목을 보면, Non-ordered Objects 에 대해서 설명이 나와있다. (OrderComparator Java Doc)

    Any object that does not provide its own order value is implicitly assigned a value of [Ordered.LOWEST_PRECEDENCE](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/Ordered.html#LOWEST_PRECEDENCE), thus ending up at the end of a sorted collection in arbitrary order with respect to other objects with the same order value.

    ⇒ 즉, order를 지정하지 않으면 지정하지 않은 Bean들은 모두 동일한 LOWEST 값을 가지게 되고, 결국에는 같은 order 값을 가지는 Bean 끼리는 임의의 순서를 가지게 된다고 나와있다.

  6. 찾아온 ControllerAdvice List를 순회하면서 exceptionHandlerAdviceCache에 추가한다.
    [ ※ exceptionHandlerAdviceCacheLinkedHashMap 이라서 추가하는 순서가 중요하다. ]

    private void initExceptionHandlerAdviceCache() {
        if (this.getApplicationContext() != null) {
            List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(this.getApplicationContext());
            Iterator var2 = adviceBeans.iterator();
    				
    				// 받아온 adviceBeans을 순회
            while(var2.hasNext()) {
                ControllerAdviceBean adviceBean = (ControllerAdviceBean)var2.next();
                Class<?> beanType = adviceBean.getBeanType();
                if (beanType == null) {
                    throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
                }
    
                ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
                if (resolver.hasExceptionMappings()) {
    		            //exceptionHandlerAdviceCache에 추가한다.
                    this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
                }
    
                if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
                    this.responseBodyAdvice.add(adviceBean);
                }
            }
        }
    }

ControllerAdvice 사용

  1. 예외가 발생하면, ExceptionHandlerExceptionResolvergetExceptionHandlerMethod에서 해당하는 method를 찾는다.

    exceptionHandlerCache는 등록한게 없기 때문에 넘어가고, 아까 위에서 열심히 등록한 exceptionHandlerAdviceCache에서 method를 찾는다.

    그리고 여기서 exceptionHandlerAdviceCache에 등록된 순서가 중요하다.
    (왜냐하면, 먼저 찾은 ControllerAdvice에서 해당 예외를 처리할 수 있으면, 더 이상 순회하지 않고 바로 처리하고 종료되기 때문이다.)

    // 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.computeIfAbsent(handlerType, ExceptionHandlerMethodResolver::new);
            Method method = resolver.resolveMethod(exception);
            //등록된게 없기 때문에 method는 null이 나온다.
            if (method != null) {
                return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method, this.applicationContext);
            }
    
            if (Proxy.isProxyClass(handlerType)) {
                handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
            }
        }
    		
    		//여기서 우리가 등록한 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;
    }

문제 해결

문제 진단 2를 통해 왜 ApiControllerAdvice를 탐색하지 않고 먼저 ServerErrorDetectControllerAdvice를 탐색하는지 알게 되었다.
(Order를 따로 지정해주지 않으면, Bean의 등록 순서에 따라 매번 임의로 순서가 정해진다.)

그렇다면 우리는 ApiControllerAdvice@Order(Ordered.HIGHEST_PRECEDENCE)를 붙여주면 정상적으로 코드가 동작을 할 것이다.
(Order를 조금 더 명확하게 드러내기 위해서, ServerErrorDetectControllerAdvice에도 @Order(Ordered.LOWEST_PRECEDENCE)를 붙였습니다.)

// ApiControllerAdvice.java

@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
@Slf4j
public class ApiControllerAdvice {
	// 중략 ...
}

// =============================================================
// ServerErrorDetectControllerAdvice.java

@Order(Ordered.LOWEST_PRECEDENCE)
@Profile("prod")
@RequiredArgsConstructor
@RestControllerAdvice
@Slf4j
public class ServerErrorDetectControllerAdvice {
	// 중략 ...
}

(※ 해당 내용을 수정한 수정 커밋)

아래의 log를 보면 수정한 내용이 정상적으로 반영이 되었음을 알 수 있다.
(원래라면, 수정하기 전에는 ServerErrorDetectControllerAdvice가 떳지만, 이제는 ApiControllerAdvice가 뜬다.)


문제 해결 짜투리

내가 글 초반에 스프링은 항상 구체적인 것을 먼저 처리한다고 이야기 했던 부분이 있는데, 이 부분을 확인 안 하고 넘어가긴 찝찝해서 이 부분에 대해서도 조금 더 찾아봤다.

ExceptionHandlerExceptionResolvergetExceptionHandlerMethod 중에 보면 ExceptionHandlerMethodResolver 객체를 가져와서 resolveMethod 메서드를 실행하는 부분이 있다.

// ExceptionHandlerExceptionResolver.class

@Nullable
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(@Nullable HandlerMethod handlerMethod, Exception exception) {
		// 중략 ..
		
		// ControllerAdvice Bean들을 순회
    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();
            // resolver.resolveMethod 메서드가 중요
            Method method = resolver.resolveMethod(exception);
            if (method != null) {
                return new ServletInvocableHandlerMethod(advice.resolveBean(), method, this.applicationContext);
            }
        }
    }

    return null;
}

resolveMethod 메서드를 타고 타고 가다 보면, getMappedMethod 메서드가 실행이 되는데, getMappedMethod 메서드가 중요하다.

// ExceptionHandlerMethodResolver.class

@Nullable
public Method resolveMethod(Exception exception) {
    return this.resolveMethodByThrowable(exception);
}

@Nullable
public Method resolveMethodByThrowable(Throwable exception) {
    Method method = this.resolveMethodByExceptionType(exception.getClass());
		// 중략 ..
}

@Nullable
public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
    Method method = (Method)this.exceptionLookupCache.get(exceptionType);
    if (method == null) {
		    // this.getMappedMethod 메서드가 중요
        method = this.getMappedMethod(exceptionType);
				
				// 중략 ..
}

getMappedMethod하나의 ControllerAdvice Bean에서 현재 발생한 예외와 가장 근접한 구체적인 예외를 찾고, 해당 예외를 처리하는 메서드를 반환한다.

// ExceptionHandlerMethodResolver.class

private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
    List<Class<? extends Throwable>> matches = new ArrayList();
    Iterator var3 = this.mappedMethods.keySet().iterator();

    while(var3.hasNext()) {
        Class<? extends Throwable> mappedException = (Class)var3.next();
        if (mappedException.isAssignableFrom(exceptionType)) {
            matches.add(mappedException);
        }
    }

    if (!matches.isEmpty()) {
        if (matches.size() > 1) {
            matches.sort(new ExceptionDepthComparator(exceptionType));
        }

        return (Method)this.mappedMethods.get(matches.get(0));
    } else {
        return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
    }
}

getMappedMethod의 동작 로직

  1. 예외와 예외를 처리하는 메서드를 key, value로 가지는 mappedMethodskeySet만 뽑아서 순회
  2. isAssignableFrom 메소드를 사용하여, key에 있는 예외 중에 현재 발생한 예외와 같거나 상위 클래스인 예외가 있는지 검사
  3. 검사하여 true가 나오면 matches에 저장
  4. matches 리스트에 하나 이상의 유형이 있으면, 이 리스트를 ExceptionDepthComparator를 사용하여 정렬
  5. 정렬을 하면 가장 구체적인 예외 (즉, 가장 근접한 예외)가 리스트의 맨 앞 부분으로 오게 됨
  6. matches 리스트의 첫 번째 요소에 해당하는 메소드를 반환 (이 메소드는 가장 구체적인 예외 유형에 매핑된 @ExceptionHandler 메소드)
  7. 이 로직을 통해 ExceptionHandlerMethodResolver는 발생한 예외 중 가장 적합한 @ExceptionHandler 메소드를 선택

따라서 더 구체적인 예외를 처리하는 메소드가 더 우선적으로 실행됨

혹시나 하고 찾아봤더니 역시나 스프링은 본인이 할 수 있는 최선을 다 하고 있었다.
(가장 구체적인 예외를 처리하는 메서드를 반환하기 위해 열심히 순회하고 있었다.)

내가 그걸 몰랐을 뿐.. 🥲


결론

  • 하나의 ControllerAdvice Bean 안에서는 최대한 구체적인 예외를 다루는 @ExceptionHandler 메소드가 실행된다.
  • 2개 이상의 ControllerAdvice Bean이 있다면, 어떤 ControllerAdvice Bean이 먼저 실행될지 모르기 때문에 @Order를 통해서 순서를 반드시 지정해줘야 한다.
    • 서로 다른 예외를 처리하고, 해당 예외끼리 겹치는게 없다면 순서를 지정하지 않아도 상관없다.

후기

  • 이번 트러블 슈팅을 진행하면서, 너무나 크게 배운 것은 “세상에 당연한 것은 하나도 없다”는 것이다.
  • 제대로 알아보지 않고 나 혼자서 “이건 당연히 맞아” 라고 생각하고 문제를 해결하려고 하니 더욱 더 잘못된 길로만 가게 되었고 문제 해결과는 점점 멀어졌다.
  • 다음부터는 확실하게 내가 알고 있는 게 맞는지부터 검증을 하고, 거기서부터 하나씩 접근을 해야겠다.

참고

profile
HW -> FW -> Web
post-custom-banner

0개의 댓글