모르지 않다는 것은 아는것과 다르다.

Spring

Spring AOP 4편 (With Spring Transaction)

채마스 2023. 4. 20. 21:09

개요

  • 3편에서는 Spring 트랜잭션(@Transactional)의 동작과정을 알아보았다.
  • 4편에서는 @Transactional 어노테이션 처리 과정을 도식화하여 정리하고, 디버깅해보도록 하자.
  • 나는 보통 코드를 분석할때, 애플리케이션 로딩 시점과 런타임 시점을 구분해서 분석한다.
    • 좀 더 자세히 말하면 아래와 같이 구분할 수 있다.

  • 먼저 애플리케이션 로딩 시점과 런타임 시점으로 나눌 수 있고, 로딩 시점에는 자동구성을 하는 시점과 스프링 컨테이너가 세팅되는 시점으로 나누어 보았다.
  • 자동구성에는 이번에 알아볼 Spirng AOP 관련된 자동구성과 Spring Transaction 관련된 자동구성에 대해서만 알아보도록 하겠다.
  • 사실 이것보다 훨씬더 깊고 복잡하지만, 나는 우선 이정도로만 Spring AOP와 Spring Transaction 정리를 해보려고 한다.
  • 나는 아래와 같이 브레이크 포인트를 정리해 보았다.
    • 만약 직접 디버깅을 해보고 싶다면 아래와 같이 브레이크 포인트를 그룹지어 활성화해서 동작시켜보면 좋을 것 같다.
    • 참고로 아래 코드는 스프링부트 2.2.5.RELEASE 버전 기준이다.

 

Spring AOP 자동구성

  • Spring AOP 관련된 자동 구성은 아래와 같은 과정을 거친다.
    1. 스프링 부트를 사용하면, spring.factories 파일에 AopAutoConfiguration 포함되어 있기 때문에 자동 설정으로 사용된다.
    2. AopAutoConfiguration 내부에 있는 AspectJAutoProxyingConfiguration가 활성화 되고, AspectJAutoProxyingConfiguration 내부에 있는 CglibAutoProxyConfiguration이 활성화되는데, CglibAutoProxyConfiguration에는 @EnableAspectJAutoProxy가 붙어있다.
    3. @EnableAspectJAutoProxy에서는 AspectJAutoProxyRegistrar를 임포트 하고, AspectJAutoProxyRegistrar에서 AnnotationAwareAspectJAutoProxyCreator를 빈으로 등록한다.
  • 이제 위의 3가지 과정을 자세히 정리해보자

1번과정

  • 스프링부트 애플리케이션 구동시, @SpringBootApplication가 활성화되고, @SpringBootApplication는 AutoConfigurationImportSelector를 임포트 하고 있다.

  • AutoConfigurationImportSelector.getCandidateConfigurations() 에서 spring-boot-autoconfigure 모듈의 spring.factories 를 읽어서 자동 구성 클래스들을 로드한다.

 

2번과정

  • AopAutoConfiguration의 내부 클래스들 중 AspectJAutoProxyingConfiguration를 보면, @ConditionalOnClass(Advice.class) 를 통해서 Advice가 존재하면 활성화 된다.
  • 또한, spring.aop.proxy-target-class를 기준으로 JDK 동적 프록시(JdkDynamicAutoProxyConfiguration)와 CGLIB 프록시(CglibAutoProxyConfiguration)를 선택하여 사용할 수 있도록 설정되어 있는 것을 확인할 수 있다.
  • JdkDynamicAutoProxyConfiguration와 CglibAutoProxyConfiguration 클래스 위에는 @EnableAspectJAutoProxy가 붙어있는데, @EnableAspectJAutoProxy를 보면 AspectJAutoProxyRegistrar를 Import하는 것을 확인할 수 있다.

 

3번과정

  • AspectJAutoProxyRegistrar 의 registerBeanDefinitions() 메소드안에 있는 AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry) 에서 AnnotationAwareAspectJAutoProxyCreator(빈 후처리기)를 빈으로 등록한다.
  • 또한 AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); 이부분을 보면, 클래스기반 프록시 생성으로 강제하는 것을 확인할 수 있다.
  • 이는 CGLIB기반으로 프록시를 생성하도록 강제하는것이다.
  • 스프링부트를 사용하면 @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true) 로 동작한다.
  • 하지만, 개발자가 proxy-target-class 값을 false로 설정하면 JDK 동적프록시 기반으로 프록시를 생성하도록 설정할 수 있다.
  • 여기서 문제는 JDK 동적프록시는 인터페이스를 기반으로 프록시를 만들기 때문에 구현체가 없는 경우에 프록시객체를 만들 수 없다는 것이다.
  • 그 경우를 대비해서 AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); 통해서 구현체가 없는 클래스의 경우에 JDK 동적프록시로 설정했다 하더라도 CGLIB로 프록시를 생성하도록 강제함으로써 위 문제를 해결한다.

 

Spring 트랜잭션 자동구성

  • AOP의 동작 과정은 어드바이저에 의해 결정된다. 따라서, 스프링 부트의 자동 구성에 의해 스프링 트랜잭션을 처리하는 AOP의 어드바이저가 어떻게 빈으로 등록되는지 알아볼 필요가 있다.
  • 내용을 정리하면 아래와 같다.
    1. 스프링 부트를 사용하면, spring.factories 파일에 TransactionAutoConfiguration이 포함되어 있기 때문에 자동 설정으로 사용된다.
    2. TransactionAutoConfiguration 내부를 보면, EnableTransactionManagementConfiguration이 있고 그 내부에는 JdkDynamicAutoProxyConfiguration이 있다. JdkDynamicAutoProxyConfiguration에는 @EnableTransactionManagement 어노테이션이 붙어 있다.
    3. @EnableTransactionManagement가 활성화되면, TransactionManagementConfigurationSelector에 의해 ProxyTransactionManagementConfiguration이 활성화된다.
    4. ProxyTransactionManagementConfiguration이 활성화되면, BeanFactoryTransactionAttributeSourceAdvisor가 어드바이저로 등록된다.
  • 이제 위의 4가지 과정을 자세히 정리해보자.

1번 과정

  • AOP 자동구성과 마찬가지로 TransactionAutoConfiguration가 spring.factories에 포함된 것을 확인할 수 있다.
  • spring.factories 파일을 보면, 아래와 같이 TransactionAutoConfiguration를 자동구성으로 사용하는 것을 알 수 있다.

 

2번 과정

  • 아래 사진은 TransactionAutoConfiguration의 내부 클래스인 EnableTransactionManagementConfiguration이다.

  • EnableTransactionManagementConfiguration의 내부 클래스로 JdkDynamicAutoProxyConfiguration가 있는데 @EnableTransactionManagement가 활성화 된 것을 확인할 수 있다.
  • 스프링은 디폴트로 CGLIB를 채택하고 있기 때문에, JDK 동적 프록시로 설정하지 않았다면 아래의 CglibAutoProxyConfiguration 가 활성화 된다.

 

3번 과정

  • 아래와 같이 @EnableTransactionManagement는 TransactionManagementConfigurationSelector를 포함하고 있다.

  • TransactionManagementConfigurationSelector에서 ProxyTransactionManagementConfiguration를 활성화한다.
  • 4번 과정부터는 이후에 좀 더 정리하도록 하자.
  • 스프링 트랜잭션관련 자동구성도 알아보았으니, 이제 본격적으로 Spring 트랜잭션 프록시 동작 과정을 알아보자.

 

4번 과정

  • ProxyTransactionManagementConfiguration이 활성화되면, BeanFactoryTransactionAttributeSourceAdvisor가 어드바이저로 등록된다.

 

애플리케이션 구동 시 (이론)

먼저 아래와 같은 서비스가 있다고 가정하자.

@Slf4j
@Service
public class SampleService {

    @Transactional
    public void save() {
        log.info("sample service save!");
    }

    public void find() {
        log.info("sample service find!");
    }
}
  • save() 메소드 위에는 @Transactional 애노테이션이 달려있다.

위의 전제하에, 애플리케이션 구동시 아래와 같은 작업이 일어난다.

  1. 먼저, BeanFactoryTransactionAttributeSourceAdvisor 에 TransactionAttributeSourcePointcut과 TransactionInterceptor를 세팅하고 BeanFactoryTransactionAttributeSourceAdvisor를 빈으로 등록한다.
  2. AbstractApplicationContext.finishBeanFactoryInitialization() 메소드를 통해서 빈을 초기화하는 작업이 이루어지는데, 이때 프록시 객체가 빈으로 등록된다.
  3. 프록시 객체를 등록하는 과정에서 어드바이저(BeanFactoryTransactionAttributeSourceAdvisor)에 등록된 포인트 컷(TransactionAttributeSourcePointcut)을 통해서 @Transactional이 붙은 클래스들을 조회한다.
  4. AbstractAutoProxyCreator 의 postProcessAfterInitialization() 메소드를 통해서 프록시 객체를 생성한다.
  5. postProcessAfterInitialization() 내부에 있는 wrapIfNecessary() 메소드 내부에서 적용될 어드바이스와 어드바이저 목록을 가져와서 프록시 객체에 세팅한다.
  6. 어드바이스와 어드바이저가 세팅된 프록시 객체가 스프링컨테이너에 등록된다. 이렇게 되면, 런타임에 @Transactional 이 붙은 메소드가 호출되는 시점에, 스프링 컨테이너에는 프록시가 등록되어 있기 때문에 실제 클래스가 아닌 프록시 객체가 호출된다.

postProcessAfterInitialization() 는 BeanPostProcessor 인터페이스의 메소드로, 스프링 컨테이너가 빈을 초기화한 후에 호출된다. 참고로, @Transactional과 관련된 기능을 처리하는 BeanPostProcessor 구현체는 AnnotationAwareAspectJAutoProxyCreator 다.

 

애플리케이션 구동 시 (디버깅)

  1. 먼저, BeanFactoryTransactionAttributeSourceAdvisor 에 TransactionAttributeSourcePointcut과 TransactionInterceptor를 세팅하고 BeanFactoryTransactionAttributeSourceAdvisor를 빈으로 등록한다.

  • advisor.setTransactionAttributeSource(transactionAttributeSource); 메소드에 transactionAttributeSource 내부에 TransactionAttributeSourcePointcut를 담고있다.
  • advisor.setAdvice(transactionInterceptor); 에서 Advice로 transactionInterceptor를 등록하는 것을 확인할 수 있다.
  1. AbstractApplicationContext.finishBeanFactoryInitialization() 메소드를 통해서 빈을 초기화하는 작업이 이루어지는데, 이때 프록시 객체가 빈으로 등록된다.

  • AbstractBeanFactory.doGetBean() 메소드에서 createBean() 를 통해서 스프링컨테이너에 등록될 빈을 생성한다.

  1. 프록시 객체를 등록하는 과정에서 어드바이저(BeanFactoryTransactionAttributeSourceAdvisor)에 등록된 포인트 컷(TransactionAttributeSourcePointcut)을 통해서 @Transactional이 붙은 클래스들을 조회한다.

  • TransactionAttributeSourcePointcut.matches() 메소드에서 모든 빈들의 모든 메서드를 필터링해서 @Transactional이 붙어있는지 검사한다.
  • 위에서 보면, SampleService의 find() 메소드는 @Transactional이 붙어있지 않기 때문에 false값을 반환한다.
  • 반면에 save() 메소드는 @Transactional가 붙어있기 때문에 true값을 반환하는 것을 확인할 수 있다.
  1. AbstractAutoProxyCreator 의 postProcessAfterInitialization() 메소드를 통해서 프록시 객체를 생성한다.

  • AnnotationAwareAspectJAutoProxyCreator의 상위 클래스인 AbstractAutoProxyCreator의 postProcessAfterInitialization() 메소드에서 프록시 객체를 생성한다.
  1. postProcessAfterInitialization() 내부에 있는 wrapIfNecessary() 메소드 내부에서 적용될 어드바이스와 어드바이저 목록을 가져와서 프록시 객체에 세팅한다.

  • 위에서 보듯이, getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); 를 통해서 빈에 적용될 어드바이저(advisor) 목록을 찾아온다.
    • 어드바이저를 찾는 과정에서 TransactionAttributeSourcePointcut가 사용된다.
    • 만약, 추가적으로 AOP를 구현했다면, 해당 빈에 적용될 Advisor가 추가로 조회되었을 것이다.

  • 그 다음, 조회된 어드바이저 목록을 프록시 객체를 생성할 때에 주입해주는 것을 확인할 수 있다.
  • 이렇게 되면, Spring AOP가 적용될 프록시 객체가 생성되는 것이다.
  1. 어드바이스와 어드바이저가 세팅된 프록시 객체가 스프링컨테이너에 등록된다.

  • AbstractBeanFactory.doGetBean() 메소드 내부에 있는 DefaultSingletonBeanRegistry.getSingleton()메소드를 호출한다.
  • DefaultSingletonBeanRegistry.getSingleton()에서 addSingleton() 메소드를 통해서 프록시 객체를 스프링컨테이너에 등록한다.
  • 이렇게 되면, 런타임에 @Transactional 이 붙은 메소드가 호출되는 시점에, 스프링 컨테이너에는 프록시가 등록되어 있기 때문에 실제 클래스가 아닌 프록시 객체가 호출된다.

 

애플리케이션 실행 중 (이론)

  • 애플리케이션 실행중에 @Transactional 애노테이션이 붙은 메소드를 호출하면 아래와 같은 작업이 일어난다.

  1. Client가 SampleController를 호출하고, SampleController를는 SampleService의 save() 메소드를 호출한다.
  2. 현재 SampleService는 CGLIB기반의 프록시로 만들어져 있기 때문에 DynamicAdvisedInterceptor.intercept() 메소드가 호출된다.
  3. TransactionInterceptor.invoke() 메소드에서 targetClass를 결정하고 invokeWithinTransaction() 메소드를 호출한다.
  4. TransactionAspectSupport.invokeWithinTransaction() 를 통해서 트랜잭션 관련 기능이 수행되고, 그 사이에 실제 타겟의 메소드인 save() 가 호출된다.
  5. 트랜잭션 로직이 수행된 결과를 다시 SampleController에게 반환한다.

애플리케이션 구동 시점에 이미 SampleService를 프록시로 만들었고, 프록시에는 트랜잭션처리와 관련된 어드바이스가 세팅되어있다. 그렇기 때문에 프록시 객체의 save() 메소드가 호출되고 실제 타겟의 save() 메소드 전후로 트랜잭션관련 로직이 실행되는 것이다.

 

애플리케이션 실행 중 (디버깅)

  • 위와 같이 Client로 부터 SampleService를 호출하는 요청을 받았다고 가정하자.
  • 또한, SampleService에는 @Transactional 애노태이션이 붙어있다.
  1. Client가 SampleController를 호출하고, SampleController를는 SampleService의 save() 메소드를 호출한다.
  2. 현재 SampleService는 CGLIB기반의 프록시로 만들어져 있기 때문에 DynamicAdvisedInterceptor.intercept() 메소드가 호출된다.

  • targetSource.getTarget() 를 통해서 프록시가 아닌 실제 SampleService를 가져온다.
  • this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass) 에서 현재 프록시 객체가 처리해야하는 어드바이스 목록이 담긴다. 아래 디버깅 결과를 보면, 트랜잭션을 처리하는 어드바이스인 TransactionInterceper가 담겨있다.
  • new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed(); 에서 process()를 통해서 어드바이스가 실행된다.
  1. TransactionInterceptor.invoke() 메소드에서 targetClass를 결정하고 invokeWithinTransaction() 메소드를 호출한다.

  • 디버깅 내용을보면, invocation이 프록시 객체이고, targetClass가 실제 객체이다.
  • 자세한 내용은 3편에서 이미 설명했기 때문에 넘어가도록 하자.
  1. TransactionAspectSupport.invokeWithinTransaction() 를 통해서 트랜잭션 관련 기능이 수행되고, 그 사이에 실제 타겟의 메소드인 save() 가 호출된다.

  • 먼저, TransactionAttributeSource를 통해서 TransactionManager를 조회한다.
  • 그 다음, createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); 를 통해서 트랜잭션을 생성한다.
  • retVal = invocation.proceedWithInvocation(); 에서 실제 타겟의 메소드인 save() 메소드가 호출된다.

  • 마지막으로 finally에서 트랜잭션의 후속 작업을 마무리한다.

  • 이 부분도 자세한 설명은 3편에서 했으니 넘어가도록 하자.

 

정리

  • 1편에서는 Spring AOP의 주요 개념인 프록시, 어드바이스, 포인트컷, 어드바이저에 대해서 알아보았다.
  • 2편에서는 Spring AOP의 동작원리에 대해서 알아보았다.
  • 3편에서는 Spring AOP의 꽃인 @Transactional의 동작원리에 대해서 알아 보았다.
  • 4편에서는 직접 디버깅을 해보며, @Transactioanl의 동작과정을 애플리케이션 구동시점과 런타임시점으로 구분지어 알아보았다.
  • 위 과정을 통해서 Spring AOP 의 대해서 좀 더 깊게 이해할 수 있었다. 이제, 실무에서 AOP 관련된 에러가 터졌을때 좀 더 직관적으로 문제를 바라볼 수 있을 것 같은 느낌이 들어서 좋았다.
  • Spring 생태계에서 AOP는 @Transactional 외에도 @Async, @Scheduled, @Cacheable, @PreAuthorize 등등.. 많은 부분에 적용되어 있다.