안녕하세요.
이번 글에서는 스프링에서 제공하는 @Transactional 어노테이션에 대해서 탐구한 내용에 대해서 정리하고자 합니다.
메소드에 @Transactional 어노테이션을 붙이는 이유는 메소드 내의 서비스에 특정 트랜잭션 정책을 적용하기 위함입니다. 또한, isolation, propagation 등의 세부적인 설정을 통해서 트랜잭션 정책을 설정할 수 있습니다. 간단하게 어노테이션을 붙임으로써 복잡한 트랜잭션 설정을 할 수 있다는 점이 흥미롭다고 판단되었고, 자주 사용하는 어노테이션인 만큼 자세한 동작 원리와 적용된 기술 등에 대해서 깊게 파악하고자 했습니다.
1. AOP를 활용한 프록시 생성
@Transactional에 사용된 첫번째 기술은 AOP입니다. Pointcut을 이용하여 @Transactional이 붙어 있는 메소드와 세부 속성을 조회합니다. 그 후, 트랜잭션 정책을 담당하는 객체인 TransactionInterceptor를 Advice로 등록하여 프록시 객체를 생성합니다. 코드는 아래와 같습니다.
@Configuration
public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {
@Bean
public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor(TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) {
BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
advisor.setTransactionAttributeSource(transactionAttributeSource);
advisor.setAdvice(transactionInterceptor);
if (this.enableTx != null) {
advisor.setOrder((Integer)this.enableTx.getNumber("order"));
}
return advisor;
}
@Bean
public TransactionAttributeSource transactionAttributeSource() {
return new AnnotationTransactionAttributeSource(false);
}
@Bean
public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource) {
TransactionInterceptor interceptor = new TransactionInterceptor();
interceptor.setTransactionAttributeSource(transactionAttributeSource);
if (this.txManager != null) {
interceptor.setTransactionManager(this.txManager);
}
return interceptor;
}
}
2. 트랜잭션 생성
메소드에 @Transactional을 추가하여 트랜잭션 정책을 적용하였으므로 아래와 같은 흐름으로 코드가 동작할 것으로 예상됩니다.
Object invoke(MethodInvocation invocation) {
startTx();
Object ret = invocation.proceed();
endTx();
return ret;
}
TransactionInterceptor와 부모 클래스인 TransactionAspectSupport의 실제 코드는 아래와 같습니다.
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
public Object invoke(MethodInvocation invocation) throws Throwable {
Class<?> targetClass = invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null;
Method var10001 = invocation.getMethod();
Objects.requireNonNull(invocation);
return this.invokeWithinTransaction(var10001, targetClass, invocation::proceed);
}
}
public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {
TransactionAttributeSource tas = this.getTransactionAttributeSource();
TransactionAttribute txAttr = tas != null ? tas.getTransactionAttribute(method, targetClass) : null;
TransactionManager tm = this.determineTransactionManager(txAttr);
PlatformTransactionManager ptm = this.asPlatformTransactionManager(tm);
String joinpointIdentification = this.methodIdentification(method, targetClass, txAttr);
TransactionInfo txInfo = this.createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
retVal = invocation.proceedWithInvocation();
} catch (Throwable var22) {
this.completeTransactionAfterThrowing(txInfo, var22);
throw var22;
} finally {
this.cleanupTransactionInfo(txInfo);
}
this.commitTransactionAfterReturning(txInfo);
return retVal;
}
}
흐름을 단계별로 정리해보면 아래와 같습니다.
- @Transactional이 적용된 Class와 Method를 조회한다.
- Class.Method에 적용된 @Transactional 속성(isolation, propagtion, readOnly 등)을 조회한다.
- 조회된 트랜잭션 속성을 참고하여 TransactionManager를 결정한다.
- 필요하다면, 트랜잭션을 생성한다.
- 트랜잭션을 시작한다.
- 실행하고자 했던 메인 서비스 Method를 호출한다.
- 트랜잭션을 커밋하거나 롤백하고 종료한다.
우선, 4단계인 '필요하다면, 트랜잭션을 생성한다.'에 초점을 맞추었습니다.
@Transactional이 선언된 메소드 내에서 또 다른 @Transactional 메소드가 중첩되어 호출되는 경우도 있기 때문에, 이러한 경우에 대응하기 위해 필요할 때만 트랜잭션이 생성되도록 createTransactionIfNecessary라고 메소드가 명명되었다고 생각했습니다.
@Transactional 관련 여러 자료에 따르면, Propagation 속성은 새로운 트랜잭션을 생성, 또는 기존 트랜잭션 사용 등의 정책을 결정함을 알 수 있었고, 이러한 내용이 어떻게 소스적으로 구현되었는지 확인하고자 하였습니다.
이러한 유연한 트랜잭션 생성 정책을 가능하게 만들었던 기술은 ThreadLocal 였습니다. 관련 소스는 아래와 같습니다.
protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
TransactionStatus status = tm.getTransaction((TransactionDefinition)txAttr);
return this.prepareTransactionInfo(tm, (TransactionAttribute)txAttr, joinpointIdentification, status);
}
protected TransactionInfo prepareTransactionInfo(@Nullable PlatformTransactionManager tm, @Nullable TransactionAttribute txAttr, String joinpointIdentification, @Nullable TransactionStatus status) {
TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification);
txInfo.newTransactionStatus(status);
txInfo.bindToThread();
return txInfo;
}
public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, ConfigurableTransactionManager, Serializable {
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
TransactionDefinition def = definition != null ? definition : TransactionDefinition.withDefaults();
Object transaction = this.doGetTransaction();
if (this.isExistingTransaction(transaction)) {
return this.handleExistingTransaction(def, transaction, debugEnabled);
}
else {
SuspendedResourcesHolder suspendedResources = this.suspend((Object)null);
return this.startTransaction(def, transaction, false, debugEnabled, suspendedResources);
}
}
}
public class JpaTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, BeanFactoryAware, InitializingBean {
protected Object doGetTransaction() {
JpaTransactionObject txObject = new JpaTransactionObject();
txObject.setSavepointAllowed(this.isNestedTransactionAllowed());
EntityManagerHolder emHolder = (EntityManagerHolder)TransactionSynchronizationManager.getResource(this.obtainEntityManagerFactory());
txObject.setEntityManagerHolder(emHolder, false);
ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(this.getDataSource());
txObject.setConnectionHolder(conHolder);
return txObject;
}
}
public abstract class TransactionSynchronizationManager {
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal("Actual transaction active");
...
}
createTransactionIfNecessary 메소드부터 doGetTransaction 메소드까지 단계별로 소스를 파악하며 원리를 파악할 수 있었습니다.
TransactionManager가 JpaTransactionManager라고 가정했을 때, EntityManager를 ThreadLocal에서 조회했기 때문에 현재 스레드에서 @Transactional 메소드가 중첩되어 실행되더라도 필요한 경우에 트랜잭션을 이어갈 수 있었습니다.
만약 ThreadLocal에서 EntityManagerHolder가 조회되었다면, 이미 스레드에서 실행 중인 스레드가 있다는 것을 의미하므로 Propagation 속성에 따라 기존 트랜잭션을 이어갈지 등의 정책을 결정합니다. 반대로 EntityManagerHolder이 존재하지 않았다면 새롭게 트랜잭션을 생성합니다. Propagation에 따른 트랜잭션 정책에 대한 자세한 설명은 생략하겠습니다.
두번째로, 3단계인 '조회된 트랜잭션 속성을 통해 TransactionManager를 결정한다'를 확인했습니다. TransactionManager 인터페이스는 JdbcTransactionManager, JPATransactionManager, KafkaTransactionManager 등 다양한 구현체를 갖고 있습니다. 그러므로, 현재 실행되는 @Transactional 메소드가 어떤 TransactionManager에 의해 처리가 필요한지 결정이 필요합니다.
protected TransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {
if (txAttr != null && this.beanFactory != null) {
String qualifier = txAttr.getQualifier();
if (StringUtils.hasText(qualifier)) {
return this.determineQualifiedTransactionManager(this.beanFactory, qualifier);
} else if (StringUtils.hasText(this.transactionManagerBeanName)) {
return this.determineQualifiedTransactionManager(this.beanFactory, this.transactionManagerBeanName);
} else {
TransactionManager defaultTransactionManager = this.getTransactionManager();
if (defaultTransactionManager == null) {
defaultTransactionManager = (TransactionManager)this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY);
if (defaultTransactionManager == null) {
defaultTransactionManager = (TransactionManager)this.beanFactory.getBean(TransactionManager.class);
this.transactionManagerCache.putIfAbsent(DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
}
}
return defaultTransactionManager;
}
} else {
return this.getTransactionManager();
}
}
TransactionInterceptor의 부모 클래스인 TransactionAspectSupport의 determineTransactionManager 메소드를 통해 어떤 TransactionManager를 사용할지 결정합니다. 기본적으로 우선순위가 가장 높은 스프링 빈 TransactionManager가 default로 사용되지만, 임의의 TransactionManager로 지정이 필요한 경우도 있을 수 있습니다. 이러한 경우에는 @Transactional 내에 직접 TransactionManager를 지정해야 합니다.
이번에는 5단계인 '트랜잭션을 시작한다'에 대해서 집중적으로 확인했습니다.
TransactionManager의 추상 클래스인 AbstractPlatformTransactionManager.doBegin 메소드를 통해 트랜잭션을 시작합니다. doBegin 메소드는 JpaTransactionManager와 같이 AbstractPlatformTransactionManager 추상 클래스를 상속한 클래스에 정의됩니다.
JpaTransactionManager.doBegin은 EntityManager와 DataSource를 설정합니다. 만약 @Transactional readOnly 속성이 true 라면, JpaTransactionManager는 EntityManager flush 정책을 수동으로 변경합니다. EntityManager의 flush가 수동(Manual)이면, JPA의 변경감지가 동작하지 않으므로 성능 상의 이점이 있으나 DB에 데이터 변경사항을 동기화하기 위해서는 강제로 flush를 해야합니다. 그러므로, readOnly는 조회 관련 트랜잭션에서 사용되는 것이 바람직합니다.
이와 관련된 코드는 아래와 같습니다.
protected void doBegin(Object transaction, TransactionDefinition definition) {
JpaTransactionObject txObject = (JpaTransactionObject)transaction;
EntityManager em = txObject.getEntityManagerHolder().getEntityManager();
int timeoutToUse = this.determineTimeout(definition);
// 아래 메소드에서 EntityManager Flush 정책을 ReadOnly 속성에 따라 결정합니다.
Object transactionData = this.getJpaDialect().beginTransaction(em, new JpaTransactionDefinition(definition, timeoutToUse, txObject.isNewEntityManagerHolder()));
txObject.setTransactionData(transactionData);
txObject.setReadOnly(definition.isReadOnly());
if (txObject.isNewEntityManagerHolder()) {
TransactionSynchronizationManager.bindResource(this.obtainEntityManagerFactory(), txObject.getEntityManagerHolder());
}
// DataSource 세팅
ConnectionHandle conHandle = this.getJpaDialect().getJdbcConnection(em, definition.isReadOnly());
ConnectionHolder conHolder = new ConnectionHolder(conHandle);
TransactionSynchronizationManager.bindResource(this.getDataSource(), conHolder);
txObject.setConnectionHolder(conHolder);
}
마지막으로 메소드를 실행하고 트랜잭션 Rollback, Commit을 결정하는 6, 7단계에 대해서 살펴보았습니다.
Rollback은 completeTransactionAfterThrowing, 그리고 Commit은 commitTransactionAfterReturning 메소드에서 담당하고 있습니다. 물론, DefaultTransactionManager는 모든 예외에 대해 롤백을 진행하지 않고, RuntimeException, Error, 이것을 상속한 예외가 발생할 때만 롤백을 진행하므로 이 점을 주의해야 합니다.
3. 요약
@Transactional은 AOP, ThreadLocal, 그리고 다형성과 상속을 활용하여 구현되어 있음을 확인할 수 있었습니다. @Transactional을 사용하는 개발자는 어떤 TransactionManager를 사용할지, 선택한 TransactionManager 내에서 readOnly, isolation, propagation 등의 설정이 어떻게 동작하는지, 또한 커밋과 롤백은 어떤 조건에 의해 진행되는지 등을 꼼꼼히 확인하여 사용해야 함을 느낄 수 있었습니다.
'기술 블로그' 카테고리의 다른 글
Java Annotation (2) | 2025.02.18 |
---|---|
Java ObjectMapper (Feat. RedisTemplate) (1) | 2025.02.13 |
스프링 Kafka Consumer Deep Dive - 1 (1) | 2025.02.05 |
Spring Security에서의 Filter (0) | 2024.12.17 |
필터와 인터셉터란? (0) | 2024.12.11 |