Skip to content

Latest commit

 

History

History
123 lines (99 loc) · 7.92 KB

File metadata and controls

123 lines (99 loc) · 7.92 KB

03 · 트랜잭션 애스펙트 — @Transactional을 위빙으로

패키지: org.springframework.transaction.aspectj

무엇을 / 왜

@Transactional의 기본 동작은 프록시다. 그러나 프록시 방식은 같은 클래스 안의 자기 호출(this.save())에 트랜잭션이 걸리지 않고, 비 public 메서드final 클래스/메서드에는 적용이 까다롭다. 도메인 객체나 레거시 코드에서 이런 제약이 문제가 될 때, AspectJ 모드가 답이 된다.

이 패키지는 @EnableTransactionManagement(mode = AdviceMode.ASPECTJ)를 켰을 때 동작하는 트랜잭션 애스펙트를 제공한다. 위빙 방식이므로 호출 경로와 무관하게, 모든 가시성의 메서드에 트랜잭션 경계가 적용된다.

핵심 타입

  TransactionAspectSupport (spring-tx)
  └─ invokeWithinTransaction(method, targetClass, callback)
     : 트랜잭션 시작 → 콜백 실행 → 커밋/롤백 — "진짜 로직"
              ▲ extends
  AbstractTransactionAspect.aj
  ├─ around(txObject) : transactionalMethodExecution(txObject)
  │     → invokeWithinTransaction(...) 어댑터
  └─ abstract pointcut transactionalMethodExecution(Object)
        ▲ extends                          ▲ extends
  AnnotationTransactionAspect.aj      JtaAnnotationTransactionAspect.aj
  ├─ AnnotationTransactionAttribute   ├─ @RequiredTypes(jakarta...Transactional)
  │  Source (Spring @Transactional)   ├─ jakarta.transaction.Transactional 매칭
  └─ pointcut = public-in-@Tx-type    └─ 동일 패턴, JTA 표준 애너테이션
                || @Tx-method

  [활성화]
  AspectJTransactionManagementConfiguration
  └─ @Bean transactionAspect = AnnotationTransactionAspect.aspectOf()
  AspectJJtaTransactionManagementConfiguration (extends 위)
  └─ @Bean jtaTransactionAspect = JtaAnnotationTransactionAspect.aspectOf()
  • AbstractTransactionAspect: TransactionAspectSupport를 상속해 around 어드바이스 하나로 그 로직에 연결한다. 어떤 메서드가 트랜잭션 대상인지는 추상 pointcut에 위임.
  • AnnotationTransactionAspect: Spring의 @Transactional(org.springframework.transaction.annotation)을 매칭하는 구체 애스펙트.
  • JtaAnnotationTransactionAspect: JTA 표준 jakarta.transaction.Transactional을 매칭. 클래스패스에 해당 애너테이션이 있을 때만 위빙되도록 @RequiredTypes로 보호.

동작 흐름 — 위빙된 @Transactional 메서드 호출

public class OrderService {           // 인터페이스 없어도, 프록시 없어도 됨
    @Transactional
    public void place(Order o) { ... }

    public void doWork() {
        place(o);   // ← self-invocation. 프록시 모드면 트랜잭션 누락. 위빙이면 OK.
    }
}
service.place(o)   (또는 this.place(o) 자기 호출)
   │
   │ AspectJ가 place() 메서드 바이트코드에 위빙해 둔 around 진입
   ▼
AbstractTransactionAspect.around(txObject)
   │   pointcut: transactionalMethodExecution(txObject)
   │   = (@Transactional 타입의 public 메서드 || @Transactional 메서드) && this(txObject)
   │
   ▼  thisJoinPoint.getSignature() → MethodSignature → Method 추출
   ▼
invokeWithinTransaction(method, txObject.getClass(), callback)
   │     (TransactionAspectSupport — spring-tx의 진짜 로직)
   │     ├─ TransactionAttributeSource로 @Transactional 속성 해석(전파/격리/롤백규칙)
   │     ├─ 트랜잭션 시작
   │     ├─ callback.proceedWithInvocation() ──▶ proceed(txObject)  // 원래 메서드 본문
   │     ├─ 정상 종료 → commit
   │     └─ 예외 → rollback 규칙에 따라 rollback
   ▼
결과 반환 (또는 예외 재던지기)

around 어드바이스의 본질은 어댑터다. AspectJ의 조인 포인트 정보(thisJoinPoint)를 spring-tx가 이해하는 Method + 콜백 형태로 변환해 invokeWithinTransaction에 넘기는 것뿐이다.

// AbstractTransactionAspect (발췌)
Object around(final Object txObject): transactionalMethodExecution(txObject) {
    MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature();
    try {
        return invokeWithinTransaction(methodSignature.getMethod(), txObject.getClass(),
            new InvocationCallback() {
                public Object proceedWithInvocation() throws Throwable {
                    return proceed(txObject);   // 원래 메서드 실행
                }
            });
    }
    catch (RuntimeException | Error ex) { throw ex; }
    catch (Throwable thr) { Rethrower.rethrow(thr); throw new IllegalStateException(...); }
}

pointcut이 잡는 두 부류

// AnnotationTransactionAspect
executionOfAnyPublicMethodInAtTransactionalType :
    execution(public * ((@Transactional *)+).*(..)) && within(@Transactional *)
    → 클래스에 @Transactional이 붙으면, 그 타입(및 서브타입)의 모든 public 메서드

executionOfTransactionalMethod :
    execution(@Transactional * *(..))
    → 메서드에 직접 @Transactional이 붙으면 가시성 불문(비 public도)

클래스 레벨 애너테이션은 public 메서드의 기본 트랜잭션 의미를 정하고, 메서드 레벨 애너테이션은 가시성과 무관하게 개별 적용·재정의한다. 비 public 메서드에 트랜잭션을 거는 유일한 방법이 메서드에 직접 애너테이션을 다는 것이라는 점이 javadoc에 명시돼 있다.

핵심 메서드 / 관용구

  • destroy()clearTransactionManagerCache(): 애스펙트는 사실상 싱글톤이므로, 컨테이너 종료 시 캐시된 TransactionManager 매핑을 비운다. DisposableBean 구현으로 라이프사이클에 연결.
  • Rethrower.rethrow(Throwable): AspectJ around는 명시적으로 선언된 checked 예외만 던질 수 있다. 그러나 위빙 대상 메서드는 임의의 checked 예외를 던질 수 있으므로, 제네릭 타입 소거를 악용해 원래 예외를 그대로 다시 던진다. "추하지만 안전한 우회"라고 주석이 인정한다.
  • aspectOf() 빈 등록: AspectJTransactionManagementConfiguration이 위버 싱글톤을 꺼내 TransactionAttributeSourceTransactionManager(있으면)를 주입한다.

설계 포인트

  • 프록시 모드와 동일 의미: 트랜잭션 시작/커밋/롤백·전파·격리 로직은 전부 TransactionAspectSupport에 있고, 프록시용 TransactionInterceptor와 이 애스펙트가 그것을 공유한다. 모드를 바꿔도 @Transactional의 의미는 같다.
  • 인터페이스 애너테이션 미상속 주의: javadoc이 강조하듯, AspectJ는 자바 규칙을 따라 인터페이스의 애너테이션을 상속하지 않는다. 따라서 @Transactional은 인터페이스가 아니라 구현 클래스(또는 그 메서드)에 달아야 한다. 이는 인터페이스 프록시도 지원하는 프록시 모드와의 차이점이다.
  • JTA 분리와 조건부 위빙: JtaAnnotationTransactionAspect@RequiredTypes("jakarta.transaction.Transactional")로, 해당 애너테이션이 클래스패스에 없으면 위빙 자체가 일어나지 않게 한다. AspectJJtaTransactionManagementConfiguration이 Spring·JTA 두 애스펙트를 함께 등록한다.

정리

트랜잭션 애스펙트는 @Transactional을 프록시 없이 바이트코드 위빙으로 적용해, self-invocation·비 public 메서드·final 타입의 제약을 없앤다. AbstractTransactionAspectaround가 조인 포인트를 Method+콜백으로 변환해 TransactionAspectSupport.invokeWithinTransaction에 넘기는 얇은 어댑터이고, 실제 트랜잭션 로직은 spring-tx와 공유한다. Spring용·JTA용 두 구체 애스펙트가 각자 pointcut만 다르게 제공하며, 애너테이션은 반드시 구현 클래스에 달아야 한다는 점이 프록시 모드와의 핵심 차이다.