소스 코드로 이해하는 @Transactional
spring-framework 5.3.x 기준으로 작성됐으며 모든 소스코드의 출처는 spring-framework입니다(출처).
@Transactional 어노테이션을 클래스, 메서드에 붙이기만 하면 Transaction이 보장된 메서드가 실행된다. 코드에 트랜잭션의 시작과 종료를 호출하는 코드를 작성할 필요가 없다. spring-data-jpa과 함께 사용할 경우에 데이터베이스의 트랜잭션 시작과 커밋(또는 롤백)이 알아서 된다.
<@Transactional 사용 예제 코드>\
@Service
public class TransactionalService {
@Transactional
public void doMultiTasks(String name) {
doTask1();
doTask2();
}
}
@Transactional 어노테이션 하나로 트랜잭션 관련 로직을 신경 쓰지 않고 비즈니스 로직에 집중할 수 있는 장점이 있다. 대충 어떠할 것이다라고는 알고 있지만 구체적으로 어떻게 동작하는지는 몰랐다. 이번 기회에 코드를 직접 보며 함께 알아보자.
비밀은 프락시(Proxy)에 있다. @Transactional을 포함하는 빈 클래스 TransactionalService는 단순히 new로 생성되는 게 아니라 프락시로 감싸져 생성된다. 프락시로 생성하는 이유는 메서드 호출을 가로채 앞 또는 뒤 (또는 앞뒤 모두)로 추가 로직을 실행하기 위해서다.
혹시 프락시의 개념을 잘 모른다면 자바의 DynamicProxy에 대해서 검색해보자
doMultiTasks() 메서드가 호출되는 시점부터 코드를 보자. 트랜잭션 메서드를 가로채는 클래스는 MethodInterceptor의 구현체 TransactionInterceptor이다. 프락시로 생성된 TransactionalService의 메서드 호출을 가로채 invokeWithinTransaction을 호출함으로써 호출된 메서드가 트랜잭션 안에서 실행되도록 한다.
<TransactionInterceptor.java>
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
@Override
@Nullable
//MethodInvocation의 Method는 doMultitasks()이다.
public Object invoke(MethodInvocation invocation) throws Throwable {
...
/*MethodInvocation을 트랜잭션 안에서 실행하는 invokeWithinTransaction를 호출한다.
*invocation: doMultiTasks()
*targetClass: TransactionalService
*/
return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {
@Override
@Nullable
public Object proceedWithInvocation() throws Throwable {
return invocation.proceed();
}
...
}
상위 클래스 TransactionAspectSupport에 정의돼있는 invokeWithinTransaction을 주석을 참고하며 읽어 내려가 보자.
<TransactionAspectSupport.java>
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class <?> targetClass,
final InvocationCallback invocation) throws Throwable {
...
//메소드와 클래스로부터 트랜잭션 속성을 가져온다.
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
/* (1) 트랜잭션을 생성하고 시작한다.
* 단 조건부로 트랜잭션 정보가 있는 경우에만 트랜잭션을 시작한다. 이름도 ...ifNecessary이다.(참고) */
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation(); // (2) doMultiTasks()가 실행된다.
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex); // (3-1) 익셉션이 발생한 경우 rollback이 호출된다
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
...
commitTransactionAfterReturning(txInfo); //(3-2) 트랜잭션을 커밋한다.
return retVal;
}
doMultiTasks 호출(2)되기 전에 트랜잭션을 시작이 시작된다(1). 만약 doMultiTasks가 익셉션을 던진다면 catch하여 롤백 한다(3-1). 정상적으로 종료됐다면 3-2에서 트랜잭션을 커밋하고 결과 값을 반환하는 것을 확인할 수 있다.
프락시로 한번 감싸진 TransactionalService의 메소드 doMultiTasks 가 호출되면 트랜잭션 인터셉터(TransactionalInterceptor)가 가로채 트랜잭션 관련 로직을 처리해준 것이다. 이 때 모든 메소드 호출에서 트랜잭션 로직이 실행되는게 아니라 잠깐 언급했듯이 조건부로 실행된다. doMultiTasks의 경우 @Transactional 어노테이션을 붙임으로써 조건에 부합한 셈이다(조건: txAtrr != null)
프락시 빈 생성에 앞서 어드바이저(Advisor)에 대해서 간단하고 알고 넘어가자. 어드바이저는 AOP의 advice와 pointcut 정보를 갖고 있는 클래스다. @Transactional에도 AOP 개념이 활용됐다. 쉽게 말해서 '어떤 클래스의 어떤 조건을 가진 메소드를 실행할 때 무슨 액션을 어느 지점에서 실행할지' 알려주는 게 어드바이저이다.
빈팩토리가 TransactionService를 빈으로 만들 때 트랜잭션 프록시로 생성하는데 어드바이저 역할을 하는 클래스가 BeanFactoryTransactionAttributeSourceAdvisor이다. 이 클래스는 spring-tx모듈에서 빈으로 정의돼 로드된다.
<ProxyTransactionManagementConfiguration.java>
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {
/*트랜잭션 어드바이저로 BeanFactoryTransactionAttributeSourceAdvisor를 등록했다.
*파라미터로 transactionAttributeSource와 transactionInterceptor를 주입받는 것을 확인할 수 있다.
*두 개의 빈은 어드바이저 아래에 선언돼있다.*/
@Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor(
TransactionAttributeSource transactionAttributeSource,
TransactionInterceptor transactionInterceptor) {
BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
advisor.setTransactionAttributeSource(transactionAttributeSource);
advisor.setAdvice(transactionInterceptor);
if (this.enableTx != null) {
advisor.setOrder(this.enableTx.<Integer>getNumber("order"));
}
return advisor;
}
/*TransactionAttributeSource의 구현체로 AnnotationTransactionAttributeSource를 주입받았다.
*TransactionAttributeSource는 트랜잭션 속성을 어디서/어떻게 가져올지 알고 있는 클래스이다.
*AnnotationTransactionAttributeSource은 그 속성들을 어노테이션으로부터 읽어오도록 구현됐다.
*ex) @Transactional*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public TransactionAttributeSource transactionAttributeSource() {
// Accept protected @Transactional methods on CGLIB proxies, as of 6.0.
return new AnnotationTransactionAttributeSource(false);
}
/* 위에서 doMultiTasks의 트랜잭션을 처리했던 TransactionInterceptor가 여기서 빈으로 생성된다.
* 그리고 BeanFactoryTransactionAttributeSourceAdvisor의 어드바이스(Advice)로 넘겨진다.*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource) {
TransactionInterceptor interceptor = new TransactionInterceptor();
//인터셉터에도 AnnotationTransactionAttributeSource을 주입해준다.
interceptor.setTransactionAttributeSource(transactionAttributeSource);
if (this.txManager != null) {
interceptor.setTransactionManager(this.txManager);
}
return interceptor;
}
...
}
BeanFactoryTransactionAttributeSourceAdvisor는 빈 생성 단계에서 빈 인스턴스 후처리 과정에서 활용된다.
<AbstractAutowireCapableBeanFactory.java>
@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) throws BeansException {
Object result = existingBean;
for (BeanPostProcessor processor : getBeanPostProcessors()) {
//초기화된 빈을 파라미터로 넘겨 후 처리한다.
Object current = processor.postProcessAfterInitialization(result, beanName);
....(중략)
return result;
}
BeanPostProcessor 중에 빈 클래스를 프락시로 감싸주는 AbstractAutoProxyCreator 구현체가 있다. 빈에 매칭 되는 어드바이저를 찾아 빈 클래스의 프락시 인스턴스에 어드바이스로 주입한다.
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
//TransactionService를 프록시로 감싼다
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
TransactionService의 프록시를 생성하기 전에 TransactionService에 매칭되는 어드바이스와 어드바이저를 가져온다.
@Override
public Object wrapIfNecessary(Class<?> beanClass, String beanName) {
...
// Create proxy if we have advice.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
... 중략
}
TransactionService의 어드바이저를 찾아서 반환하는데 어드바이저가 없는 경우 DO_NOT_PROXY를 반환한다.
@Override
protected Object[] getAdvicesAndAdvisorsForBean(
Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {
//TransactionalService(beanClass)에 해당하는 Advisor를 찾는다.
List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY; //advisor가 없는 경우 '프락시 하지 않는다'를 반환한다
}
return advisors.toArray(); //Proxy 인스턴스에 활용될 advisor(인터셉터)를 반환한다
}
...
한 단계 더 들어가 보자. 모든 어드바이저를 반환하는게 아니라 후보가 되는 어드바이저를 가져온 후에 그중에서 TransactionService에게 eligible(적합한) 어드바이저를 골라낸다.
protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
//findEligibleAdvisor를 살펴보면 후보가 되는 candidateAdvisors에서
// beanClass에 적용할 수 있는 eligibleAdvisor를 솎아낸다.
List<Advisor> candidateAdvisors = findCandidateAdvisors();
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
...(중략)
return eligibleAdvisors;
}
이렇게 타고 들어가다 보면 eligible한 어드바이저인지 여부를 AopUtils.canApply 으로 판별하는 것을 확인할 수 있다. 주석을 보며 읽어내려가보자.
/*PointCut은 BeanFactoryTransactionAttributeSourceAdvisor이 가진
* TransactionAttributeSourcePointcut가 넘겨진다.*/
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
Assert.notNull(pc, "Pointcut must not be null");
/*PointCut(TransactionAttributeSourcePointcut)의 classFitler로 TransactionalService가 *PointCut에 매칭 되는지 필터링한다.
*내부적으로 AnnotationTransactionAttributeSource의 isCandidateClass()가 호출된다(소스코드).*/
if (!pc.getClassFilter().matches(targetClass)) {
return false;
}
...(중략)
//PointCut의 MethodMatcher를 가져온다
MethodMatcher methodMatcher = pc.getMethodMatcher();
...(중략)
//TransactionalService의 모든 부모와 인터페이스를 뽑아낸다.
Set<Class<?>> classes = new LinkedHashSet<>();
if (!Proxy.isProxyClass(targetClass)) {
classes.add(ClassUtils.getUserClass(targetClass));
}
classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
/*인터페이스와 클래스들의 메서드를 순회하며 BeanFactoryTransactionAttributeSourceAdvisor
*와 PointCut과 매칭여부를 확인 후 매칭 하는 경우 true를 반환한다.*/
for (Class<?> clazz : classes) {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
for (Method method : methods) {
if (introductionAwareMethodMatcher != null ?
introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) :
methodMatcher.matches(method, targetClass)) { <-- methodMather의 matches 실행
//TransactionAttributeSourcePointcut과 매칭 되는 메서드가 있으면 true를 반환한다.
return true;
}
}
}
...(중략)
}
BeanFactoryTransactionAttributeSourceAdvisor의 MethodMatcher는 TransactionAttributeSourcePointcut이다.
<TransactionAttributeSourcePointcut.class>
@Override
public boolean matches(Method method, Class<?> targetClass) {
//TransactionAttribute로 AnnotationTransactionAttributeSource를 가져온다.
TransactionAttributeSource tas = getTransactionAttributeSource();
// 타깃이 되는 메서드에서 트랜잭션 어트리뷰트가 존재하는 경우 True를 반환한다.
return (tas == null || tas.getTransactionAttribute(method, targetClass) != null);
}
트랜잭션 관련 속성이 있는 빈 클래스라면 true를 반환한다. TransactionAttributeSource의 구현체로 AnnotationTransactionAttributeSource가 주입이 됐던 게 기억나는가? AnnotationTransactionAttributeSource은 @Transactional 어노테이션에서 속성 값을 가져온다. doMultiTasks()는 @Transactional 어노테이션이 붙어 있으므로 속성 값이 null이 아니므로 matches()가 true를 반환한다.
<AnnotationTransactionAttributeSource.java>
@Nullable
protected TransactionAttribute determineTransactionAttribute(AnnotatedElement element) {
//트랜잭션 어노테이션 파서들을 순회하며 엘리먼트(메서드 또는 클래스)를 파싱 한다
for (TransactionAnnotationParser parser : this.annotationParsers) {
TransactionAttribute attr = parser.parseTransactionAnnotation(element);
//파싱 된 속성이 존재하는 경우 해당 속성 값(TransactionAttribute)을 반환한다.
if (attr != null) {
return attr;
}
}
return null;
}
TransactionService의 advisor로 BeanFactoryTransactionAttributeSourceAdvisor를 가져왔으니 다시 BeanPostProcessor로 돌아가서 빈과 어드바이저로 프록시를 생성하자.
<AbstractAutoProxyCreator.java>
@Override
public Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
...
//specificInterceptors에 담긴 BeanFactoryTransactionAttributeSourceAdvisor가 포함된다.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
//DO_NOT_PROXY를 반환하지 않았으므로 프락시를 생성한다.
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
//어드바이저를 파라미터로 넘겨 프락시 인스턴스를 생성한다.
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}
//매칭되는 어드바이저가 없는 빈의 경우 그대로 반환한다.
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
@Transaction이 붙은 메서드가 있는 빈 클래스는 빈 초기화 후에 BeanPostProcessor를 통해서 트랜잭션을 지원하는 프락시로 감싸진다. 따라서 해당 메소드가 실행될 때 트랜잭션 관련 로직이 앞뒤로 실행된다.
1. BeanPostProcessor가 TransactionService를 프락시로 감싼다(AbstractAdvisorAutoProxyCreator)
2. 트랜잭션의 어드바이저는 BeanFactoryTransactionAttributeSourceAdvisor이다
3. AnnotationTransactionAttributeSource가 @Transactional을 파싱 하고 속성 정보를 읽는다
4. TransactionInterceptor가 @Transaction 메서드 호출을 가로채 트랜잭션 관련 로직을 처리한다.