Spring @Transactional 1: 프록시와 AOP
TOC
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 호출 체인에 등록된 TransactionInterceptor의 invoke가 실행된다. 인터셉터는 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를 통해 메소드 호출 전후에 동작을 추가하는 기능이다.
다음 글에서는 TransactionInterceptor가 TransactionManager를 통해 트랜잭션을 시작하고 종료하는 과정을 살펴본다.