..

Spring @Transactional 1: 프록시와 AOP

TOC


  1. Overview
  2. @Transactional은 메소드를 직접 바꾸지 않는다
  3. 인터셉터에서 TransactionManager 호출하기
  4. Lombok과는 무엇이 다른가
  5. Conclusion

Overview


앞선 글에서 트랜잭션은 여러 데이터베이스 작업을 하나의 논리적 작업 단위로 묶는 장치라고 정리했다.

이번에는 Spring에서 @Transactional이 실제로 어떻게 동작하는지 살펴본다.

@Transactional
public void transfer(Long fromId, Long toId, long amount) {
    accountRepository.withdraw(fromId, amount);
    accountRepository.deposit(toId, amount);
}

메소드 본문에는 트랜잭션을 시작하거나 커밋하는 코드가 없다. @Transactional은 컴파일 시 메소드 본문을 변경하는 방식이 아니라, 런타임에 Spring이 빈을 프록시로 감싸고 메소드 호출 전후에 AOP 로직을 적용하는 방식으로 동작한다.

@Transactional은 메소드를 직접 바꾸지 않는다


@Transactional은 메소드 내부에 코드를 삽입하는 어노테이션이 아니다. Spring은 애플리케이션 컨텍스트를 구성할 때 어노테이션을 트랜잭션 메타데이터로 읽고, 해당 빈에 트랜잭션 어드바이스를 연결한다.

클라이언트가 주입받는 객체는 다음처럼 원본 객체를 감싼 프록시일 수 있다.

AccountService bean
├─ Spring AOP 프록시
│  └─ 원본 AccountService

따라서 호출 흐름은 원본 메소드로 바로 진입하는 것이 아니라 다음과 같다.

클라이언트
  → Spring AOP 프록시
  → TransactionInterceptor
  → 원본 메소드

런타임 환경에서 @Transactional이 붙은 메소드가 프록시를 통해 호출되면, TransactionInterceptor는 해당 메소드의 트랜잭션 속성을 확인한다. 그 후 실제 메소드가 실행되기 전(before invocation)에 트랜잭션을 시작하고, 메소드 실행이 후(after invocation)에 결과에 따라 커밋 또는 롤백을 처리한다.

런타임 메소드 호출
  → TransactionInterceptor가 @Transactional 속성 확인
  → 메소드 실행 전: 트랜잭션 시작
  → 실제 메소드 실행
  → 메소드 실행 후: 커밋 또는 롤백

인터셉터에서 TransactionManager 호출하기


프록시를 통해 메소드가 호출되면 AOP 호출 체인에 등록된 TransactionInterceptorinvoke가 실행된다. 인터셉터는 TransactionManager를 의존하고 있으며, invoke 내부에서 실제 트랜잭션 시작과 종료를 요청한다.

// 실제 Spring 구현을 단순화한 의사 코드
public Object invoke(MethodInvocation invocation) throws Throwable {
    TransactionStatus status = transactionManager.getTransaction(transactionDefinition);

    try {
        // 프록시 다음에 있는 원본 객체의 메소드를 실행한다.
        Object result = invocation.proceed();
        transactionManager.commit(status);
        return result;
    } catch (Throwable exception) {
        transactionManager.rollback(status);
        throw exception;
    }
}

호출 관계를 정리하면 다음과 같다.

클라이언트
  → 프록시의 호출 체인
  → TransactionInterceptor.invoke()
  → TransactionManager.getTransaction()
  → invocation.proceed()
  → 원본 메소드 실행
  → TransactionManager.commit() 또는 rollback()

위 코드는 이해를 위한 단순화된 형태다. 실제 Spring은 예외의 롤백 규칙, 기존 트랜잭션 참여 여부, 전파 속성, 리소스 정리와 동기화 콜백 등을 함께 처리한다. 하지만 인터셉터가 TransactionManager를 호출한 뒤 proceed()로 원본 메소드를 실행한다는 구조는 동일하다.

Lombok과는 무엇이 다른가


Spring의 @Transactional과 Lombok 모두 어노테이션을 사용하지만 동작 시점이 다르다.

  • @Transactional: 런타임에 프록시와 AOP 인터셉터로 동작한다.
  • Lombok: 컴파일 시 annotation processor가 생성자, getter, builder 등의 코드를 만든다.

예를 들어 @RequiredArgsConstructor는 실행 시 프록시를 만드는 것이 아니라 컴파일 전에 생성자를 추가한다.

@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
}

결국 @Transactional은 런타임 동작을 연결하고, Lombok은 컴파일 시 코드를 생성한다.

Conclusion


@Transactional의 핵심은 메소드 안에 트랜잭션 코드를 작성하는 것이 아니다. Spring이 원본 빈을 프록시로 감싸고, 프록시가 메소드 호출을 가로채 트랜잭션 경계를 적용하는 것이다.

@Transactional은 어노테이션 하나로 끝나는 기능이 아니라, 프록시와 AOP를 통해 메소드 호출 전후에 동작을 추가하는 기능이다.

다음 글에서는 TransactionInterceptorTransactionManager를 통해 트랜잭션을 시작하고 종료하는 과정을 살펴본다.

다음 글: TransactionManager의 시작과 종료