스프링 AOP에서 UndeclaredThrowableException 가 발생해요

김재연·2024년 6월 2일
3

배경

리뷰하고 있는 프로젝트에서 AOP를 적용하다가 ControllerAdvice로 안가고 UndeclaredThrowableException 가 발생한다고 질문받았다. 처음보는 예외여서 어떻게 동작하는지 확인을 해보았다.

문제 됬던 프로젝트에서 Aspect를 이용하는데, 조건에 맞지 않으면 AuthenticationException 를 던지는데, AuthenticationException 가 아닌 UndeclaredThrowableException 를 던졌다.

// AOP를 사용하는 곳
@AopAnnotation
@PostMapping
public ApiResponse test() {
 	 
}

// Aspect 로직
@Override
public String intercept(HttpServletRequest request) throws ExceptionX {
    if(조건) throw new ExceptionX(); // 여기서 예외가 터졌음
    return pass;
}

// 예외
public class ExceptionX extends Exception {
...
}

원인

AspectJ를 이용하여 AOP를 구현하면 CglibAopProxy.proceed() 를 호출하는데, 해당 메서드는 RuntimeExcpetion, Exception에 따라 로직이 다르게 동작한다.

public Object proceed() throws Throwable {
    try {
        return super.proceed();
    } catch (RuntimeException var2) {
        RuntimeException ex = var2;
        throw ex;
    } catch (Exception var3) {
        Exception ex = var3;
      
        // 여기가 중요
        if (!ReflectionUtils.declaresException(this.getMethod(), ex.getClass()) && !KotlinDetector.isKotlinType(this.getMethod().getDeclaringClass())) {
            throw new UndeclaredThrowableException(ex);
        } else {
            throw ex;
        }
    }
}

문제 됬던 상황에서는 Exception을 상속받았기 때문에 예외가 터졌을때 2번째 catch문으로 이동을 하게 되었고, if절에 있는 조건이 true가 되서 UndeclaredThrowableException 가 발생했다.

ReflectionUtils.declaresException 의 상세 구현은 아래와 같다.

  1. 1번에서는 메서드에 선언된 예외의 목록을 조회
  2. 발생한 예외가 declaredExceptions에 있는지 체크
  3. true를 반환하면 AuthenticationException 예외를 던지고, false를 반환하면 위에서 throws UndeclaredThrowableException 를 던진다.
public static boolean declaresException(Method method, Class<?> exceptionType) {
		Assert.notNull(method, "Method must not be null");
		Class<?>[] declaredExceptions = method.getExceptionTypes(); // 1번 여기가 중요!!!
		for (Class<?> declaredException : declaredExceptions) { // 2번
			if (declaredException.isAssignableFrom(exceptionType)) {
				return true;
			}
		}
		return false;
	}

해결방안

  • 1안 예외던지는곳에서 Exception을 상속받은 예외가 아닌 RuntimeException 예외를 상속받은 예외 던진다.
  • 2안 메서드에 throws ExceptionX 를 던진다.
// 2안 해결방안
@AopAnnotat안on
@PostMapping
public ApiResponse test() throws ExceptionX {
 	 
}

결론

  • 2안을 사용하면 AOP를 사용하는 모든 메서드 시그니처에 throws Exception를 선언해야 되므로 깔끔하지 못하다. 따라서 예외를 던지는 곳에서 RuntimeException을 상속받은 Unchecked Exception을 던지면 좋을거 같다.
profile
이제 블로그 좀 쓰자

0개의 댓글