다양한 시각 시리즈 3번째 트랜잭션입니다!
트랜잭션 어노테이션을 적용시켰을 때 어떤 일들이 일어나는지에 대해 자세히 적어보았습니다.
@Transactional
public void removeEvents(List<Adate> adates) {
adateRepository.deleteAll(adates);
}
이렇게 @Transactional 어노테이션을 붙이면 어떻게 될까 알고 싶어 작성한 글입니다.
트랜잭션을 호출하게 되면 스프링은 트랜잭션이 필요한 빈에 대해 프록시를 생성합니다. (위의 경우 removeEvents가 포함되어 있는 Service 클래스에 대해 프록시를 생성합니다)
이후, TransactionInterceptor에 의해 해당 메서드의 실행 전후에 트랜잭션의 시작과 종료가 자동으로 처리됩니다.
TransactionInterceptor은 MethodInterceptor을 구현하고 TransactionAspectSupport를 상속했습니다.
먼저 구현체인 MethodInterceptor을 살펴보겠습니다.
@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
/**
* Implement this method to perform extra treatments before and
* after the invocation. Polite implementations would certainly
* like to invoke {@link Joinpoint#proceed()}.
* @param invocation the method invocation joinpoint
* @return the result of the call to {@link Joinpoint#proceed()};
* might be intercepted by the interceptor
* @throws Throwable if the interceptors or the target object
* throws an exception
*/
@Nullable
Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}
MethodInterceptor
우선 MethodInterceptor은 @FunctionalInterface 어노테이션을 가지고 있습니다.
이는 MethodInterceptor가 함수형 인터페이스라는 뜻이며 그로인해 추상 메서드가 하나 있습니다.
함수형 인터페이스의 특징을 간략하게 설명하면 함수를 일급 객체로 사용할 수 없는 자바 언어의 단점을 보완하기 위해 java8에 도입되었습니다. 함수형 인터페이스는 구현해야 할 추상 메서드가 하나만 정의되었습니다.
자바의 람다식을 이용하면 함수를 일급 객체로 다룰 수 있기 때문에 함수형 프로그래밍 패러다임을 자바에서도 적용할 수 있게 되었습니다. Runnable, Callable, Comparator, Predicate, Function, Consumer, Supplier 등이 있습니다.
일급 객체란 밑의 3가지 조건을 충족한 객체를 일컫는다.
1. 모든 일급 객체는 변수나 데이터에 담을 수 있어야 한다.
2. 모든 일급 객체는 함수의 파라미터로 전달할 수 있어야 한다.
3. 모든 일급 객체는 함수의 리턴값으로 사용할 수 있어야 한다.
Implement this method to perform extra treatments before and after the invocation.
Polite implementations would certainly like to invoke {@link Joinpoint#proceed()}.
@param invocation the method invocation joinpoint
@return the result of the call to {@link Joinpoint#proceed()}; might be intercepted by the interceptor
@throws Throwable if the interceptors or the target object throws an exception
다시 MethodInterceptor로 돌아와서 유일한 추상 메서드인 invoke에 대해 살펴보겠습니다.
invoke의 뜻인 "불러오다"로 유추할 수 있듯이 invoke는 메서드 호출 전 후에 추가 처리를 수행하기 위한 함수입니다.
그리고 파라미터로 받아오는 MethodInvocation 클래스는 불러온 메서드에 대한 정보를 가지고 있습니다.
public interface MethodInvocation extends Invocation {
/**
* Get the method being called.
* <p>This method is a friendly implementation of the
* {@link Joinpoint#getStaticPart()} method (same result).
* @return the method being called
*/
@Nonnull
Method getMethod();
}
MethodInvocation
getMethod 함수를 통해 호출 전 후를 처리해야 할 메서드를 가져올 수 있습니다.
여기까지 알면 자연스레 TransactionInterceptor의 invoke는 자연스레 트랜잭션의 호출 전 후 추가 처리를 위한 함수구나! 인 것을 알 수 있습니다.
TrasactionInterceptor
@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();
}
});
}
TransactionInterceptor
코드를 천천히 살펴보겠습니다.
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
invocation.getThis를 통해 프록시 대상 객체를 가져옵니다.
getThis 코드
/**
* Return the object that holds the current joinpoint's static part.
* <p>For instance, the target object for an invocation.
* @return the object (can be null if the accessible object is static)
*/
@Nullable
Object getThis();
그런 뒤, AopUilts.getTargetClass 메서드를 통해 실제 타겟 클래스를 정합니다.
/**
* Determine the target class of the given bean instance which might be an AOP proxy.
* <p>Returns the target class for an AOP proxy or the plain class otherwise.
* @param candidate the instance to check (might be an AOP proxy)
* @return the target class (or the plain class of the given object as fallback;
* never {@code null})
* @see org.springframework.aop.TargetClassAware#getTargetClass()
* @see org.springframework.aop.framework.AopProxyUtils#ultimateTargetClass(Object)
*/
public static Class<?> getTargetClass(Object candidate) {
Assert.notNull(candidate, "Candidate object must not be null");
Class<?> result = null;
if (candidate instanceof TargetClassAware targetClassAware) {
result = targetClassAware.getTargetClass();
}
if (result == null) {
result = (isCglibProxy(candidate) ? candidate.getClass().getSuperclass() : candidate.getClass());
}
return result;
}
AopUtils
코드를 보면 파라미터를 통해 받은 candidate가 TargetClassAware 인터페이스를 구현했는지 확인합니다.
이때 TargetClassAware는 스프링에서 제공하는 클래스이며, getTargetClass 메서드를 통해 프록시가 감싸고 있는 클래스를 반환할 수 있습니다. TargetClassAware를 구현하는 경우는 프록시가 적용된 경우입니다.
/**
* Minimal interface for exposing the target class behind a proxy.
*
* <p>Implemented by AOP proxy objects and proxy factories
* (via {@link org.springframework.aop.framework.Advised})
* as well as by {@link TargetSource TargetSources}.
*
* @author Juergen Hoeller
* @since 2.0.3
* @see org.springframework.aop.support.AopUtils#getTargetClass(Object)
*/
public interface TargetClassAware {
/**
* Return the target class behind the implementing object
* (typically a proxy configuration or an actual proxy).
* @return the target Class, or {@code null} if not known
*/
@Nullable
Class<?> getTargetClass();
}
TargetClassAware
프록시 적용하는 모습도 보여줘!
당연하죠ㅎㅎ 밑의 코드는 ProxyFactory에서 프록시 객체를 생성하는 다양한 방법 중 한 가지입니다.
TargetSource는 TargetClassAware를 상속한 인터페이스로 프록시가 호출할 실제 객체를 나타냅니다.
package org.springframework.aop.framework;
public class ProxyFactory extends ProxyCreatorSupport {
...
/**
* Create a proxy for the specified {@code TargetSource} that extends
* the target class of the {@code TargetSource}.
* @param targetSource the TargetSource that the proxy should invoke
* @return the proxy object
*/
public static Object getProxy(TargetSource targetSource) {
if (targetSource.getTargetClass() == null) {
throw new IllegalArgumentException("Cannot create class proxy for TargetSource with null target class");
}
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTargetSource(targetSource);
proxyFactory.setProxyTargetClass(true);
return proxyFactory.getProxy();
}
...
}
ProxyFactory
간단히 설명하면 ProxyFactory 객체를 생성한 뒤, targetSource를 프록시 팩토리에 저장합니다.
그리고 프록시가 호출할 실제 대상 객체를 지정하고, 프록시가 대상 클래스를 확장하도록 설정합니다.
그런 뒤, getProxy를 통해 프록시를 반환합니다.
이 방법 외에도 다양한 방법으로 프록시가 대상 클래스를 확장하도록 설정합니다.
public static Class<?> getTargetClass(Object candidate) {
Assert.notNull(candidate, "Candidate object must not be null");
Class<?> result = null;
if (candidate instanceof TargetClassAware targetClassAware) {
result = targetClassAware.getTargetClass();
}
if (result == null) {
result = (isCglibProxy(candidate) ? candidate.getClass().getSuperclass() : candidate.getClass());
}
return result;
}
AopUtils
다시 getTargetClass로 돌아와서 candidate가 targetClassAware을 구현했으면 getTargetClass를 통해 프록시가 감싸고 있는 객체를 가져옵니다.
그런 뒤, 객체가 CGLIB 프록시인지 JDK 동적 프록시인지 확인합니다.
간단히 설명하자면, JDK 동적 프록시는 인터페이스를 구현하여 프록시 객체를 생성합니다.
그리고 CGLIB 프록시는 바이트코드를 조작하여 대상 클래스의 서브 클래스를 생성하고 이를 통해 프록시를 만듭니다.
그러므로 클래스에 대한 프록시를 생성할 수 있습니다. (Spring Boot AOP에서는 CGLIB를 기본값으로 사용합니다)
CGLIB 프록시이면 슈퍼 클래스를 가져옵니다(CGLIB는 서브 클래스를 생성한 뒤, 프록시로 만들기 때문), 그리고 CGLIB 프록시가 아니면 클래스를 그대로 가져옵니다.
@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();
}
});
}
TransactionInterceptor
이제 여러분은 targetClass를 가져오는 부분을 이해했습니다.
요약하면, 스프링은 트랜잭션이 적용된 빈에 프록시를 적용하고, TransactionInterceptor을 통해 메서드 호출을 가로채며, invocation.getThis()를 사용하여 프록시 대상 객체가 null인지 아닌지를 확인합니다. 대상 객체가 있다면 AopUtils의 getTargetClass를 통해 실제 대상 클래스를 가져오고 대상 객체가 null이면 null을 반환합니다.
이제는 호출된 메서드, 클래스를 실제 트랜잭션 관리 로직을 실행하는 코드를 설명하겠습니다.
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();
}
});
TransactionInterceptor
위 코드를 설명하기 전에 invokeWithinTransaction 함수에 대해 자세히 설명하겠습니다.
@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.
TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
final TransactionManager tm = determineTransactionManager(txAttr);
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager rtm) {
boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
boolean hasSuspendingFlowReturnType = isSuspendingFunction &&
COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName());
if (isSuspendingFunction && !(invocation instanceof CoroutinesInvocationCallback)) {
throw new IllegalStateException("Coroutines invocation not supported: " + method);
}
CoroutinesInvocationCallback corInv = (isSuspendingFunction ? (CoroutinesInvocationCallback) invocation : null);
ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
Class<?> reactiveType =
(isSuspendingFunction ? (hasSuspendingFlowReturnType ? Flux.class : Mono.class) : method.getReturnType());
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(reactiveType);
if (adapter == null) {
throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
method.getReturnType());
}
return new ReactiveTransactionSupport(adapter);
});
InvocationCallback callback = invocation;
if (corInv != null) {
callback = () -> KotlinDelegate.invokeSuspendingFunction(method, corInv);
}
Object result = txSupport.invokeWithinTransaction(method, targetClass, callback, txAttr, rtm);
if (corInv != null) {
Publisher<?> pr = (Publisher<?>) result;
return (hasSuspendingFlowReturnType ? KotlinDelegate.asFlow(pr) :
KotlinDelegate.awaitSingleOrNull(pr, corInv.getContinuation()));
}
return result;
}
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.
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
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);
}
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
else {
Object result;
final ThrowableHolder throwableHolder = new ThrowableHolder();
// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
try {
result = cpptm.execute(txAttr, status -> {
TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
try {
Object retVal = invocation.proceedWithInvocation();
if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
return retVal;
}
catch (Throwable ex) {
if (txAttr.rollbackOn(ex)) {
// A RuntimeException: will lead to a rollback.
if (ex instanceof RuntimeException runtimeException) {
throw runtimeException;
}
else {
throw new ThrowableHolderException(ex);
}
}
else {
// A normal return value: will lead to a commit.
throwableHolder.throwable = ex;
return null;
}
}
finally {
cleanupTransactionInfo(txInfo);
}
});
}
catch (ThrowableHolderException ex) {
throw ex.getCause();
}
catch (TransactionSystemException ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
ex2.initApplicationException(throwableHolder.throwable);
}
throw ex2;
}
catch (Throwable ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
}
throw ex2;
}
// Check result state: It might indicate a Throwable to rethrow.
if (throwableHolder.throwable != null) {
throw throwableHolder.throwable;
}
return result;
}
}
TransactionAspectSupport
1. ReactiveTransactionManager를 사용하는 경우
반응형 트랜잭션에서는 ReactiveTransactionManager를 직접 사용해서 트랜잭션을 관리해야 합니다. 주로 실시간 데이터 처리, 고성능 웹 애플리케이션, 마이크로서비스 아키텍처등에서 ReactiveTransactionManager를 사용합니다.
기존의 트랜잭션과 차이점은 ReactiveTransactionManager은 트랜잭션 상태를 ThreadLocal이 아닌 Reactor Context에 바인딩하여 처리합니다. 이는 트랜잭션이 여러 스레드에 걸쳐 비동기적으로 실행될 때 트랜잭션 상태를 안전하게 관리할 수 있게 합니다.
- https://spring.io/blog/2019/05/16/reactive-transactions-with-spring
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/ReactiveTransactionManager.html
2. PlatformTransactionManager를 사용하는 경우
트랜잭션의 상태를 관리하는 전통적인 방식은 ThreadLocal에 트랜잭션 상태를 저장하는 것입니다.
이 방식은 스프링에서 선언적 트랜잭션 관리를 지원하는 @Transactional 어노테이션에서 주로 사용됩니다.
스프링의 @Transactional 어노테이션은 PlatformTransactionManager를 사용하여 트랜잭션을 관리합니다. PlatformTransactionManager는 여러 트랜잭션 매니저를 구현한 인터페이스로, 예를 들어 JdbcTransactionManager, JpaTransactionManager, HibernateTransactionManager 등이 있습니다.
각각의 트랜잭션 매니저는 해당 기술 스택에 맞게 데이터베이스 연결 및 트랜잭션 처리를 지원합니다.
@Transactional 어노테이션을 사용할 때, 해당 메서드나 클래스에서 정의한 트랜잭션 경계(Transaction Boundaries)를 기반으로 스프링이 트랜잭션을 시작하고 관리합니다. 이는 메서드가 호출될 때, 예외가 발생했을 때 등의 상황에서 트랜잭션을 롤백할 수 있도록 지원합니다.
필자는 선언적 트랜잭션을 사용하므로 PlatformTransactionManager에 대해 설명하겠습니다.
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
트랜잭션 매니저를 PlatformTransactionManager로 변경합니다.
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
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
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);
}
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
트랜잭션 관리자가 CallbackPreferringPlatformTransactionManager가 아닌 경우 표준 트랜잭션 처리를 수행합니다.
createTransactionIfNecessary 트랜잭션을 생성합니다.
이후, try-catch-finally에서 invocation.proceedWithInvocation()으로 실제 비즈니스 메서드를 호출합니다.
proceedWithInvocation을 통해 다음 인터셉터가 있다면 다음 인터셉터를 호출합니다.
일반적으로 이는 대상 객체가 호출되는 결과를 가져옵니다.
이때 다음 인터셉터란?
예를 들어 로깅 인터셉터, 보안 인터셉터, 트랜잭션 인터셉터가 있을 때 주문을 하면 로그를 기록하면서 다음 인터셉터인 보안으로 넘깁니다. 그리고 보안에서는 트랜잭션으로 넘기고 트랜잭션에서는 대상 객체를 호출합니다. (즉 메서드가 실행되는 것입니다)
이러한 인터셉터의 분리를 통해 관심사를 분리할 수 있습니다. (AOP의 핵심 기능)
예외가 발생하면 completeTransactionAfterThrowing을 통해 롤백을 합니다.
finally에서는 cleanupTransactionInfo를 통해 트랜잭션 정보를 정리합니다.
commitTransactionAfterReturning(txInfo);
return retVal;
이후 트랜잭션을 커밋하고 결과 값을 반환합니다.
전체 코드
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.
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
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);
}
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
TransactionAspectSupport
이 코드는 CallbackPreferringPlatformTransactionManager를 사용하는 트랜잭션 처리 로직입니다.
else {
Object result;
final ThrowableHolder throwableHolder = new ThrowableHolder();
// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
try {
result = cpptm.execute(txAttr, status -> {
TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
try {
Object retVal = invocation.proceedWithInvocation();
if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
return retVal;
}
catch (Throwable ex) {
if (txAttr.rollbackOn(ex)) {
// A RuntimeException: will lead to a rollback.
if (ex instanceof RuntimeException runtimeException) {
throw runtimeException;
}
else {
throw new ThrowableHolderException(ex);
}
}
else {
// A normal return value: will lead to a commit.
throwableHolder.throwable = ex;
return null;
}
}
finally {
cleanupTransactionInfo(txInfo);
}
});
}
catch (ThrowableHolderException ex) {
throw ex.getCause();
}
catch (TransactionSystemException ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
ex2.initApplicationException(throwableHolder.throwable);
}
throw ex2;
}
catch (Throwable ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
}
throw ex2;
}
// Check result state: It might indicate a Throwable to rethrow.
if (throwableHolder.throwable != null) {
throw throwableHolder.throwable;
}
return result;
}
TransactionAspectSupport
CallbackPreferringPlatformTransactionManager은 PlatformTransactionManager를 확장하며, 트랜잭션 코드를 실행할 때 콜백 방식을 선호하는 시나리오에 적합합니다.
PlatformTransactionManager 방식과 비교했을 때 예외 처리가 복잡하다는 특징과 롤백을 결정하는 방식이 다릅니다.
콜백이 필요한 유스케이스에서 사용하는 것이 좋을 것 같습니다.
CallbackPreferringPlatformTransactionManager에 대한 지식이 없어 자세한 설명은 생략하겠습니다...ㅎㅎ
댓글로 요청해 주시면 한번 공부해 보고 작성해 볼게요 ^_^
그러면 이제 여러분은 프록시에서 대상 객체를 가져오는 방법과 트랜잭션 관리 로직을 실행하는 방법까지 알게 되었습니다.
@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();
}
});
}
위의 코드에서는 CoruoutinesInvocationCallback의 익명 함수를 정의해 invokeWithinTransaction 메서드를 실행시킵니다.
정리
정리를 하자면 SpringBoot는 트랜잭션이 적용된 클래스에 대해 프록시를 생성하고 트랜잭션에서는 AopUtils.getTargetClass를 통해 프록시로 감싼 대상 클래스를 가져옵니다.
그런 뒤, invokeWithinTransaction 함수를 통해 트랜잭션 관리를 실행시키며 선언 트랜잭션의 경우 (@Transaction) PlatformTransactionManager를 통해 트랜잭션을 관리합니다.
PlatformTransactionManager은 ThreadLocal에 트랜잭션 상태를 저장하며, 예외가 발생했을 때 롤백을 지원합니다.