쟈미로그

[Spring] 스프링의 AOP 본문

Spring Boot

[Spring] 스프링의 AOP

쟈미 2023. 6. 19. 02:07

서론

소프트웨어를 개발할 때, 핵심 비즈니스 로직 외에도 기능 동작에 필요한 것들을 개발해야한다. (ex. 로깅, 트랜잭션 등)

그런데 만약 이런 부가적인 기능들이 자주 사용되는 것이라면 어떻게 될까?

코드에는 비즈니스 로직 + 부가적인 기능들이 뒤섞여서 핵심 로직을 찾기 어렵게 된다..

 

AOP(관점 지향 프로그래밍)는 이런 상황에서 도움이 된다!

AOP는 핵심 로직과 부가적인 기능으로 관심사를 분리하고, pointcut을 정의해서 관심사간에 코드를 침투하지 않고도 사용하길 원하는 코드가 적용되도록 한다.

 

 

1. 스프링의 AOP

스프링에서도 자체 AOP 프레임워크를 통해서 AOP를 지원한다.

스프링 AOP는 Proxy 기반으로 동작하는데, 이 프록시를 구현하는 방식이 2가지다.

 

JDK Dynamic Proxy

Java의 리플렉션 패키지에 있는 Proxy 클래스를 통해서 프록시 객체를 생성하는 방식.

이 방식은 프록시 객체를 만들기 위해서, 타깃 클래스의 인터페이스를 기준으로 프록시 객체를 만든다.

(=> 인터페이스와 그 구현체로 타깃 클래스를 정의해야지, 프록시 객체를 만들 수 있음)

CGLib (Code Generation Library)

클래스의 바이트코드를 조작해서 프록시 객체를 생성하는 방식.

이 방식은 프록시 객체를 만들기 위해서, 타깃 클래스를 상속받아서 프록시 객체를 만든다.

(=> 타깃 클래스와 프록시 객체가 부모-자식 관계임)

 

원래 스프링은 외부 라이브러리 의존성 문제, 성능 등등의 이유로 JDK Dynamic Proxy 방식을 채택했었다. 하지만 시간이 흐르면서 스프링 4.3/스프링부트 1.4부터 AOP 프록시 생성 시 디폴트로 CGLib 방식이 채택됐다!!

(이 블로그에 이유와 과정이 잘 설명돼있음. 참고하자)

 

 

2. CGLib 동작 방식

프록시 생성에는 크게 Enhancer, Callback와 부가적으로 CallbackFilter가 필요하다.

  • Enhancer : 프록시 클래스/객체를 생성하는 클래스.
  • Callback : 프록시로 들어오는 호출을 가로채는 인터페이스. 
    • CGLib 사용 시 주로 사용되는 Callback 구현체는 MethodInterceptor다.
  • CallbackFilter : 적용하려는 CallBack을 지정할 수 있는 인터페이스.

 

 

글로 설명하는 것보다, 실제로 디버깅해보면서 프록시가 만들어지는 과정을 따라가보는 게 직관적으로 이해가 잘 되는 것 같다.

따라가본 과정을 정리해보자!

디버깅 환경 : 스프링부트 2.7.12

 

(1) 프록시 빈이 생성되는 시기

Spring context가 빈 후보들을 스캔해서 빈을 생성할 때 프록시 빈과 그냥 빈을 만드는 방식이 갈린다.

우선 빈 생성 과정에서 AbstractAutoProxyCreatorwrapIfNecessary 메소드가 호출되는데, 이 메소드는 프록시 빈으로 만들 빈을 프록시로 래핑해주는 역할을 한다.

 

그럼 프록시로 래핑되는 기준 (= 프록시 빈으로 등록되는 기준)은 뭘까?

코드를 보면 프록시인지 아닌지를 판단하는 분기문 위에서 getAdvicesAndAdvisorsForBean 메소드를 호출해서 해당 클래스에 Advice(interceptor)가 등록돼있는지 찾는다. 즉, 클래스에 Advice(interceptor)가 걸려있으면 프록시로 래핑되는 것.

    // Create proxy if we have advice.
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    if (specificInterceptors != DO_NOT_PROXY) {
        this.advisedBeans.put(cacheKey, Boolean.TRUE);
        Object proxy = createProxy(
                bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }

 

프록시 빈을 생성하는 시기를 알았으니, createProxy 메소드를 타고가보면서 프록시 빈이 본격적으로 어떻게 만들어지는지 알아보자.

 

 

(2) 프록시 빈을 만드는 과정

 

ProxyFactory 구현체인 CglibAopProxy에서 프록시 빈을 만들 준비를 한다.
이 클래스의 getProxy 메소드를 보면, 

1. Enhancer 객체를 만들고

2. 타겟 클래스를 superClass로 세팅하고 (CBLib은 타겟 클래스를 상속받는 형태로 프록시 객체를 만드므로..)

3. callback 메소드들을 세팅해서

enhancer가 프록시 클래스를 만들 수 있는 준비 과정을 거친다.

    Enhancer enhancer = createEnhancer();
...
    enhancer.setSuperclass(proxySuperClass);
    enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
...
    Callback[] callbacks = getCallbacks(rootClass);
...

 

그 후 ObjenesisCglibAopProxy 클래스의 createProxyClassAndInstance 메소드를 호출해서 본격적으로 프록시 빈 인스턴스를 만든다.

1. 세팅된 Enhancer의 createClass 메소드를 호출해서 타겟 클래스를 상속받은 프록시 클래스를 만들고

2. 생성한 프록시 클래스의 인스턴스를 만들기 위해 기본 빈 생성자를 기준으로 프록시 객체가 만들어진다. <- ?
이렇게 기본 빈 생성자를 통해서, 타겟클래스의 필드들이 null로 된 프록시 빈이 생기는 것이다!

 

(코드 캡쳐 첨부 수정하기)

 

 

참고

https://www.baeldung.com/cglib
https://eminentstar.tistory.com/76
https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html

'Spring Boot' 카테고리의 다른 글

[Spring] Spring의 예외 처리 구조  (0) 2023.03.23
Comments