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

Spring

Spring AOP 3편 (With Spring 트랜잭션)

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

개요

  • 2편에서는 Spring AOP의 적용방법에 대해서 알아보았다.
  • 3편에서는 Spring AOP의 적용사례중 가장 많이 사용되는 스프링 트랜잭션(@Transactional)의 동작 방식을 알아보도록 하자.

 

Spring 트랜잭션 프록시 동작 과정

  • 이전편에서 프록시, 어드바이스, 포인트컷, 어드바이저 에 대해서 알아보았다.
  • Spring 트랜잭션도 AOP 기반으로 동작하기 때문에 위 4가지 요소를 집고 넘어가야한다.

 

프록시 (in Spring 트랜잭션)

  • @Transactional 애노테이션이 클래스나 메소드에 붙어있으면, 트랜잭션 AOP는 해당 클래스를 프록시로 만들어서 스프링컨테이너에 등록한다.

  • 이전편에서 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기가 포인트 컷을 사용해서 @Transactional이 붙은 클래스를 찾은 뒤, 해당 클래스를 프록시 객체로 감싸서 스프링 컨테이너에 저장한다고 설명했다.

  • AnnotationAwareAspectJAutoProxyCreator는 AbstractAutoProxyCreator를 상속하고 있으며, AbstractAutoProxyCreator의 postProcessAfterInitialization() 메소드에서 wrapIfNecessary() 를 호출하여 프록시 객체를 생성한다.

  • wrapIfNecessary() 메소드 내부를 보면, getAdvicesAndAdvisorsForBean() 메소드를 통해서 적용될 어드바이스 및 어드바이저들을 가져온다.
  • createProxy() 메소드를 통해서 프록시를 생성할때, 조회한 어드바이스 및 어드바이저를 프록시 객체에 세팅한다.
  • 예를들어 아래와 같이 SampleService가 있다고 가정하자
@Slf4j
public class SampleService {

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

    public void find() {
        log.info("sample service find!");
    }
}
  • save() 메소드 위에 @Transactional이 붙어 있기 때문에 SampleService는 스프링 컨테이너에 빈으로 등록될 때, 프록시 객체로 등록된다.
  • 따라서 디버깅하다보면 SampleService가 sampleService$$CGLIB 이런식으로 CGLIB기반 프록시로 저장된 것을 확인할 수 있다.
  • 사실, 스프링부트를 사용하고 있는 환경에서 SampleService를 빈으로 등록하게 되면, @Transactional 애노테이션을 붙이지 않았더라도 스프링컨테이너에는 CGLIB 기반의 프록시가 등록이 된다.
  • 하지만, @Transactional 애노테이션을 붙이지 않으면 @Transactional에 해당하는 어드바이스는 적용되지 않는다.

 

포인트 컷 (in Spring 트랜잭션)

  • 결론부터 말하자면, TransactionAttributeSourcePointcut 이 @Transactional 애노테이션을 감지하는 포인트컷이다.

  • 1편에서 어드바이저 = 포인트컷 + 어드바이스 라고 언급한적이 있다.
  • 그렇기 때문에 위에서 보듯 BeanFactoryTransactionAttributeSourceAdvisor가 TransactionAttributeSourcePointcut를 가지고 있는 것을 확인할 수 있다.
  • TransactionAttributeSourcePointcut은 2가지 용도로 사용된다.
    • 첫 번째, 애플리케이션 구동시 @Transactional 애노테이션이 붙은 클래스를 찾을 때 사용된다.
    • 두 번째, 런타임에 @Transactional 애노테이션이 붙은 메소드를 호출하는 시점에 Advice가 적용될 위치를 찾을 때 사용된다.
    • 두가지 경우를 아래와 같이 구분지어 설명할 수 있다.

[첫 번째(구동시), 두 번째(런타임)] TransactionAttributeSourcePointcut의 matchs() 메소드에서 getTransactionAttribute() 호출하는데, getTransactionAttribute() 에서 캐시된 속성이 있는지 확인하고, 없다면 computeTransactionAttribute() 메소드를 호출한다.

  • 애플리케이션 구동 시점에는 당연히 캐시된 속성이 없기 때문에 else 블록으로 동작한다.
  • 하지만, 런타임에는 캐시된 attributeCache가 있기 때문에 if (cached != null)이 실행된다.

[두 번째(런타임)] computeTransactionAttribute() 내부를 보면, findTransactionAttribute() 메소드가 있는데 findTransactionAttribute() 는 아래와같이 2가지 종류가 있다.

  • 파라미터가 Method인 것은 method 위에 @Transactional 애노테이션이 붙은 것을 감지하는데 사용된다.
  • 파라미터가 Class<?>인 것은 클래스 위에 @Transactional 애노테이션이 붙은 것을 감지하는데 사용된다.
  • findTransactionAttribute() 메소드에서 determineTransactionAttribute() 메소드를 호출하는 것을 확인할 수 있다.

[두 번째(런타임)] determineTransactionAttribute() 내부에서 parseTransactionAnnotation() 를 호출한다.

  • parseTransactionAnnotation()에서는 AnnotatedElement를 통해서 @Transactional를 찾는다.
  • @Transactional가 있다면, 그 어노테이션의 속성을 추출하고 parseTransactionAnnotation(AnnotationAttributes attributes) 메서드를 호출한다.

[두 번째(런타임)] parseTransactionAnnotation(AnnotationAttributes attributes)에서 AnnotationAttributes 를 통해서 트랜잭션 속성을 추출한다.

  • 트랜잭션의 전파 방식, 격리 수준, 타임아웃, 읽기 전용 여부, 트랜잭션 관리자의 빈 이름, 롤백 및 노롤백 규칙 등을 설정한 뒤, RuleBasedTransactionAttribute 를 반환하는 것을 확인할 수 있다.

 

어드바이스 (in Spring 트랜잭션)

  • 위에서 ProxyTransactionManagementConfiguration이 활성화 되는 과정을 알아 보았다.
  • ProxyTransactionManagementConfiguration의 내부 코드를 보면 아래와 같다.

  • 위의 코드에서 advisor.setAdvice(transactionInterceptor); 에서 확인할 수 있듯이 transactionInterceptor를 어드바이스로 등록한다.
  • transactionInterceptor가 트랜잭션 AOP에서 트랜잭션 기능(부가기능)을 수행하는 어드바이스인 것이다.
  • 따라서 transactionInterceptor 내부 코드를 보면 트랜잭션 기능들을 확인할 수 있다. 아래 코드를 분석해 보자.

  • 1편에서 언급했듯이, AOP의 Advice를 구현하기 위해서는 MethodInterceptor를 구현하면 된다.
  • MethodInterceptor를 구현하면 invoke(MethodInvocation invocation) 메소드를 오버라이드하면 되는데, methodInvocation에 프록시 객체 인스턴스, 인수(args), 메소드 정보 등이 담겨있다.
    • invocation.getThis() 가 프록시 객체이다.
    • AopUtils.getTargetClass(invocation.getThis()) 를 통해서 원본 클래스를 찾아서 TransactionAspectSupport의 invokeWithinTransaction()메소드에 전달한다.
  • invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) 메소드에 어드바이스(부가기능)로직이 담겨있다.
    • 즉, 트랜잭션처리에 대한 구현이 담겨있는 것이다.
    • targetClass가 프록시 객체가 가지고 있는 원본 클래스이다.
  • 이제부터 트랜잭션 처리의 핵심 로직인 invokeWithinTransaction() 메소드의 구현을 살펴보자.
  • 2가지 과정으로 나누어서 코드를 분석해보자
  • 1번 과정: 트랜잭션 매니저 조회

  • getTransactionAttributeSource() 메소드를 통해서 TransactionAttributeSource를 가져오고, TransactionAttributeSource로 TransactionAttribute를 조회한다.
  • TransactionAttribute로 TransactionManager를 조회한다.
  • ReactiveTransactionManager를 사용할지를 체크하고 사용하지 않는다면, PlatformTransactionManager를 사용한다.
  • 마지막으로 methodIdentification() 메소드를 통해서 joinpointIdentification 를 생성한다.
  • joinpointIdentification를 통해서 생성된 joinPoint는 트랜잭션 처리 과정에서 로깅 또는 모니터링을 수행하는 데 사용된다.
  • 2번 과정: @Transactional이 붙은 메소드 실행 (핵심로직)

  • 위의 코드가 트랜잭션을 처리하는 핵심로직이다.
  • 표준 트랜잭션 여부 판단: if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) 에서 txAttr이 null인 경우, 표준 트랜잭션 처리가 필요없는 것으로 간주한다.
    • 하지만, 이 경우에도 트랜잭션 시작, 커밋, 롤백 등의 처리를 안전하게 수행하기 위해서 표준트랜잭션 처리를 하는것인데, txAttr를 확인해서 트랜잭션 생성을 건너 뛴다.
    • CallbackPreferringPlatformTransactionManager 가 아닌 경우, 표준 트랜잭션 처리를 진행하는 것이다.
    • 기본적으로 스프링에서 제공하는 PlatformTransactionManager 구현체들은 CallbackPreferringPlatformTransactionManager를 구현하고 있지 않기때문에, @Transactional 애노테이션이 붙은경우 !(ptm instanceof CallbackPreferringPlatformTransactionManager) 이 조건 때문에 해당 if 블록을 탄다고 생각하면 된다.
  • 트랜잭션 시작: createTransactionIfNecessary() 를 통해서 트랜잭션을 시작한다.
  • 메서드 실행: invocation.proceedWithInvocation() 를 통해서 @Transactional이 붙은 실제 메소드를 실행한다.
  • 예외 처리: 예외 발생시 completeTransactionAfterThrowing() 를 통해서 트랜잭션을 롤백 한다.
  • 트랜잭션 자원 정리: cleanupTransactionInfo() 를 통해서 트랜잭션 정보를 정리한다.
  • 트랜잭션 커밋: 메서드가 정상적으로 완료된 경우, commitTransactionAfterReturning() 를 통해서 트랜잭션을 커밋 한다.
  • if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) 조건에서 else 블록을 타는경우는 사용자가 직접 구현한 PlatformTransactionManager가 CallbackPreferringPlatformTransactionManager 인터페이스를 구현하거나, 써드 파티 라이브러리에서 제공하는 구현체가 이 인터페이스를 구현하는 경우이다.
    • 그렇기 때문에 트랜잭션 매니저를 따로 커스텀하지 않는 이상 else블록이 실행되는 경우는 거의 없다.
    • 따라서 코드 설명은 넘어가도록 하겠다...ㅎㅎ

 

이번편에서 Spring Transaction 의 동작방식에 대해서 알아보았다. 다음 편해서는 직접 디버깅을 해보며, 좀 더 자세하게 @Transactional의 동작원리에 대해서 알아보자.