@Pointcut에 @target을 사용할 경우 NPE 발생

eora21·2023년 12월 28일
1

문제점

최신 스프링 부트(3.2.1)버전에서 @target을 사용해 포인트컷을 지정해주려 했습니다.

@Pointcut("@target(toby.aop_ltw.controller.HelloAnnotation)")
public void helloAnnotation() {
}

그러자 아래와 같은 예외가 발생했습니다.

Cannot invoke "org.apache.commons.logging.Log.isDebugEnabled()" because "this.logger" is null

@target이 아닌 @within은 아무 문제가 없었습니다.

@target으로 지정한 어노테이션이 사용중이 아니라 해도 예외는 똑같이 발생했습니다.
예외는 OncePerRequestFilter.doFilter, GenericFilterBean.init 부분에서 발생했습니다.

Unable to proxy interface-implementing method [public final void org.springframework.boot.web.servlet.RegistrationBean.onStartup(jakarta.servlet.ServletContext) throws jakarta.servlet.ServletException] because it is marked as final, consider using interface-based JDK proxies instead.

final이 선언된 메서드에 의해 Class 기반 프록시가 불가능하니 인터페이스 기반 JDK 프록시를 이용하라는 메시지입니다.

두 메서드에 final이 선언되어 있는 것을 확인할 수 있습니다.

명확한 에러를 보이는 필터들은 RequestContextFilter, characterEncodingFilter, formContentFilter입니다.

각각 필터들은 OncePerRequestFilter를 extend하고 있습니다. 이로 인해 세 필터의 생성 과정에서 문제가 발생함을 유추할 수 있습니다.

문제 발생 지점 예상

CGLib

SpringBoot에서는 CGLib 프록시가 사용됩니다.

spring.aop.auto=true
spring.aop.proxy-target-class=true

이 두 설정이 기본적으로 사용되고 있기 때문입니다.
위는 자동 @EnableAspectJAutoProxy 설정이고, 밑은 클래스 기반 프록시(CGLib 프록시) 설정입니다.

전에는 인터페이스 중심의 Dynamic 프록시가 사용되었으나, 스프링 3.2 버전부터 CGLib 프록시의 단점들(기본 생성자 필요, 생성자 2번 실행, 종속성 추가)이 해소되며(라이브러리를 통해 기본 생성자 없이도 생성 가능 & 생성자 1번만 실행, Spring Core에 종속성 기본 포함) default proxy 방법을 CGLib로 선택하게 됩니다.

@target을 사용하면

@target은 해당 클래스 뿐만이 아닌, 슈퍼 클래스들의 어노테이션도 살펴봅니다.
해당 스캐닝에 의해 필터들이 CGLib 프록시를 시도하며 생기는 이유 같습니다.

@args를 사용하면

@target과 같은 상황이 발생합니다.

args(..)을 사용하면

args(..)은 모든 빈들을 프록시화한다는 것과 같습니다.
이 역시도 필터 및 몇몇 클래스에서 final로 인해 CGLib 프록시를 사용하지 못 한다는 경고문이 발생합니다.

다만 @target과 달리 더 많은 빈들에 대해 경고가 출력됩니다. 이는 @target의 스캐닝에 의한 것만은 아님을 보여줍니다. 만약 스캐닝 과정이 문제였다면, args(..)와 같이 모든 final 선언 메서드에 의한 경고가 떴어야 합니다.

이 사실로 미루어 보았을 때, RequestContextFilter, characterEncodingFilter, formContentFilter들이 @target에 적합하다는 결과가 발생하였고 이로 인해 CGLib 프록시가 진행되려 했으나 final 메서드에 의해 예외가 발생했다고 추론할 수 있습니다.

선 생성, 후 적용

프록시를 생성하는 시점은 애플리케이션이 만들어질 때 입니다.
포인트컷을 통해 AOP를 실행하기 위해선 실행 시점에 프록시가 존재해야 하기 때문에, 스프링은 빈들에 대해 프록시 객체를 만들고자 합니다.
따라서 넓은 범위의 포인트컷이 있을 경우, 스프링은 애플리케이션 생성 시점에 모든 빈에 대한 프록시를 만들고자 합니다.
@target@args(..)에서 경고 발생 수의 차이는 해당 포인트컷의 적용 범위의 차이라고 볼 수도 있겠습니다.

그렇다면 @target은 어떠한 이유로 filter들을 proxy로 만들어야 한다는 결정을 내린 걸까요? 그것도 사용하지 않는 어노테이션을 지정했는데 말이죠.

filter 구조 때문일까?

저는 해당 어노테이션(@HelloAnnotation)을 만들어뒀으나, 그 어디에도 사용하지 않았습니다.
@Inherited도 해당 어노테이션에 적용하지 않았기 때문에, 결론적으로는 해당 필터들에 어노테이션이 직접적으로 선언되지 않았다면 AOP의 대상이 될 리가 없습니다.
또한 @within은 전혀 이와 같은 문제가 발생하지 않았으므로, filter들은 해당 어노테이션을 직접적으로 가지고 있지 않다고도 볼 수 있습니다.

filter들의 특수한 구조 때문에 위와 같은 문제가 일어나는 것인지.. 혹은 @target이 특정한 범위를 가져서 그런 건지.. 아리송합니다.

회피책

@target의 대상을 고를 때 직접 작성한 코드들만 대상으로 삼도록 설정하여 해당 문제를 회피하였습니다.

@Pointcut("execution(* toby..* (..)) && @target(toby.aop_ltw.controller.HelloAnnotation)")
public void helloAnnotation() {
}

그러나 해당 문제를 확실히 파악하지 못 하였다는 점은 변치 않습니다. 계속 찾아 볼 생각입니다.

profile
나누며 타오르는 프로그래머, 타프입니다.

0개의 댓글