Spring

Transactional 어노테이션에 대해 알아보자

정한_s 2023. 7. 21. 15:59

스프링에서 간단히 @Transactional을 적는 것으로, 쉽게 트랜잭션을 구현할 수 있습니다. 어떻게 이 과정이 수행되는지 궁금했고 코드를 보면서 분석을 해보았습니다.

우선 @Transactional 어노테이션은 AOP 기능을 기반으로 동작을 합니다. Spring에서 AOP는 두가지 방법으로 구현을 합니다.

AOP에 대한 자세한 내용은 해당 포스트에 좋은 글이 있어 공유 합니다. ( https://steady-coding.tistory.com/608 )


AOP란?

더보기

AOP는 이러한 교차 관심사를 분리하여 별도의 모듈로 만들고, 이를 필요한 시점에 적용하도록 합니다. 이렇게 함으로써 기능별로 코드를 분리하고 재사용성을 높이며, 유지 보수성을 향상시킬 수 있습니다. AOP(Aspect-Oriented Programming)는 교차 관심사(cross-cutting concerns)를 모듈화하는 프로그래밍 패러다임입니다. 교차 관심사는 여러 클래스나 메서드에서 공통으로 적용되는 기능을 말합니다. 예를 들어, 로깅, 보안, 트랜잭션 관리 등이 이에 해당합니다.

  • 다이나믹 프록시 방식
    • 특징인터페이스를 기반으로 프록시 객체를 생성한다
    • 리플렉션을 통해서 프록시 객체를 만든다.
  • CGLIB 방식
    • 특징타겟의 클래스를 상속받아 프록시를 생성한다.
    • 클래스의 바이트 코드를 조작하여 프록시 객체를 만든다.

 

@Transactional 어노테이션을 사용할 때 기본으로는 CGLIB를 기반으로 작동합니다.

// 기본 값은 true 입니다. 
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(TransactionManager.class)
@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
public static class EnableTransactionManagementConfiguration {

    @Configuration(proxyBeanMethods = false)
    @EnableTransactionManagement(proxyTargetClass = false)
    @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
            matchIfMissing = false)
    public static class JdkDynamicAutoProxyConfiguration {

    }

    @Configuration(proxyBeanMethods = false)
    @EnableTransactionManagement(proxyTargetClass = true)
    @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
            matchIfMissing = true)
    public static class CglibAutoProxyConfiguration {

    }

}

설정 파일을 바꾸어서 JdkDynamicAopProxy를 사용할 수 있습니다

// appication.yml
spring:
  aop:
    proxy-target-class: false

CglibAopProxy에서 TransactionIntercepter 를 호출하고 invoke 명령어를 통해서 TransactionAspectSupport을 호출합니다.

	// TransactionIntercepter.java
	@Override
	@Nullable
	public Object invoke(MethodInvocation invocation) throws Throwable {
		// Work out the target class: may be {@code null}.
		// The TransactionAttributeSource should be passed the target class
		// as well as the method, which may be from an interface.
		Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

		// Adapt to TransactionAspectSupport's invokeWithinTransaction...
		return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {
			@Override
			@Nullable
			public Object proceedWithInvocation() throws Throwable {
				return invocation.proceed();
			}
			@Override
			public Object getTarget() {
				return invocation.getThis();
			}
			@Override
			public Object[] getArguments() {
				return invocation.getArguments();
			}
		});
	}

그렇다면 TransactionAspectSupport은 어떻게 동작하는 지 살펴 봅시다

@Nullable
	protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
			final InvocationCallback invocation) throws Throwable {

		// If the transaction attribute is null, the method is non-transactional.
		// transaction 정보를 가져온다
		TransactionAttributeSource tas = getTransactionAttributeSource();
		final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
		final TransactionManager tm = determineTransactionManager(txAttr);
		// Reactive Transaction Manager 처리 
		if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager rtm) {
			....
		}

		PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
		final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
		// Standard Transaction Manager 처리
		if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm)) {
			// Standard transaction demarcation with getTransaction and commit/rollback calls.
			TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
			...
		}
		// Callback Preferring Transaction Manager 처리
		else {
			Object result;
			final ThrowableHolder throwableHolder = new ThrowableHolder();

			// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
			....
	}

메서드는 주로 세 부분으로 나눌 수 있으며, 각각 다른 유형의 트랜잭션 매니저를 처리하는 방법에 대해 설명하고 있습니다.

  1. Reactive Transaction Manager: 이 부분은 ReactiveTransactionManager를 처리합니다. 반응형 프로그래밍을 위해 사용되는 트랜잭션 매니저입니다. 코루틴이 지원되는지 체크하고, 코루틴 함수의 경우에 대한 별도 처리를 합니다. 트랜잭션 내에서 메서드를 호출하고 결과를 반환합니다.
  2. Standard Transaction Manager: 이 부분은 일반적인 PlatformTransactionManager를 처리합니다. 트랜잭션을 시작하고, 메서드를 호출한 뒤, 에러가 발생하면 롤백하고, 아니면 커밋합니다. Vavr 라이브러리를 사용하는 경우에 대한 추가 처리를 합니다.
  3. Callback Preferring Transaction Manager: 이 부분은 CallbackPreferringPlatformTransactionManager를 처리합니다. 트랜잭션을 시작하고, 메서드를 호출한 뒤, 결과를 반환합니다. 트랜잭션 내에서 예외가 발생하면 롤백하고, 아니면 커밋합니다.

저희는 Standard Transaction Manager를 사용하므로 Standard Transaction Manager를 좀 더 자세히 살펴 보겠습니다.

PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
		final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

		if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm)) {
			// Standard transaction demarcation with getTransaction and commit/rollback calls.
			// 트랜잭션이 시작되지 않았다면 Transaction 설정을 기반으로 Transaction을 시작합니다. 
			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();
			}
			catch (Throwable ex) {
				
				// target invocation exception
				// transaction 이후에 exception이 있는 경우 transaction 처리 
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
			finally {
				cleanupTransactionInfo(txInfo);
			}
			// 함수형 인터페이스 체크 
			if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
				// Set rollback-only in case of Vavr failure matching our rollback rules...
				TransactionStatus status = txInfo.getTransactionStatus();
				if (status != null && txAttr != null) {
					retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
				}
			}
			// commit 처리  
			commitTransactionAfterReturning(txInfo);
			return retVal;
		}
  1. 처음에는 tm이라는 TransactionManager를 PlatformTransactionManager로 변환합니다.
  2. 트랜잭션이 시작되지 않았다면 Transaction 설정을 기반으로 Transaction을 시작합니다.
  3. 그리고나서 실제 비즈니스 로직을 실행합니다. 만약 로직 실행 중에 오류가 발생하면, 트랜잭션을 롤백하거나 커밋하고 오류를 처리합니다. 로직 실행이 끝나면 트랜잭션 정보를 정리합니다.
  4. 이 과정에서, 만약 Vavr라는 라이브러리를 사용하여 함수형 프로그래밍이 이루어졌다면, 실패한 경우에 트랜잭션 상태를 변경할 수 있도록 추가적인 처리를 합니다.
  5. 마지막으로, 비즈니스 로직의 실행이 성공적으로 끝나면 트랜잭션을 커밋합니다. 만약 이 과정에서 오류가 발생하면 적절하게 처리합니다.
  6. 마지막으로 비즈니스 로직의 결과를 반환합니다.

그렇다면 rollback 처리는 어떻게 이루어지는 지 확인합니다. rollback 처리는 completeTransactionAfterThrowing에서 처리를 합니다.

protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
		if (txInfo != null && txInfo.getTransactionStatus() != null) {
			if (logger.isTraceEnabled()) {
				logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
						"] after exception: " + ex);
			}
			if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
				try {
					txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
				}
				catch (TransactionSystemException ex2) {
					logger.error("Application exception overridden by rollback exception", ex);
					ex2.initApplicationException(ex);
					throw ex2;
				}
				catch (RuntimeException | Error ex2) {
					logger.error("Application exception overridden by rollback exception", ex);
					throw ex2;
				}
			}
			else {
				// We don't roll back on this exception.
				// Will still roll back if TransactionStatus.isRollbackOnly() is true.
				try {
					txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
				}
				catch (TransactionSystemException ex2) {
					logger.error("Application exception overridden by commit exception", ex);
					ex2.initApplicationException(ex);
					throw ex2;
				}
				catch (RuntimeException | Error ex2) {
					logger.error("Application exception overridden by commit exception", ex);
					throw ex2;
				}
			}
		}
	}

실제로 존재하고 아직 완료되었는지 확인을 위해, 먼저 txInfo와 txInfo의 TransactionStatus가 null이 아닌지 확인합니다.

이후 txInfo.transactionAttribute.rollbackOn(ex)가 true를 반환하면, 즉 현재 예외가 롤백을 유발해야 하는 경우, 트랜잭션을 롤백합니다. 만약 false를 반환한다면, 트랜잭션을 커밋합니다.

그렇다면 txInfo.transactionAttribute.rollbackOn(ex) 는 어떻게 판별하는 지 알아봅시다.

기본으로는 DefaultTransactionAttribute의 rollbackOn을 사용합니다. 코드는 다음과 같습니다

@Override
	public boolean rollbackOn(Throwable ex) {
		return (ex instanceof RuntimeException || ex instanceof Error);
	}

ex이 Error와 RuntimeException에 대해서 true를 return 하고 있습니다

 

결론

  • @Transactional 은 AOP 기반으로 동작하며 default는 CGLIB이다
  • @Transactional은 일반적으로 PlatformTransactionManager 을 사용한다.
  • rollback의 경우는 Error | RuntimeException 이 반환되었을 때 한다

 

https://techblog.woowahan.com/2606/

https://private-space.tistory.com/98

https://steady-coding.tistory.com/608

https://sup2is.github.io/2021/03/04/java-exceptions-and-spring-transactional.html