👨‍💻 프로그래밍/📦 Backend

Spring에서 Transaction 관리하는 법 알아보기

by 개발자 진개미 2024. 5. 6.
반응형


기존 Transaction의 문제점

🐜 Transaction을 위한 코드가 너무 많이 필요함

사실 Transaction은 핵심 Business Logic이 아니기 때문에 알아서 해 줬으면 좋겠는데, Transaction을 직접 관리할 경우 이것만을 위한 코드가 너무 많이 필요합니다. 

public void placeOrder(Order order) {
    // Transaction의 속성을 만들고
    TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
    // 새로운 Transaction을 시작하고
    TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);

    try {
        // 비즈니스 로직을 실행한 뒤...
        saveOrder(order);
        updateInventory(order);

        // 성공하면 Commit
        transactionManager.commit(transactionStatus);
    } catch (Exception ex) {
        // 실패하면 Rollback
        transactionManager.rollback(transactionStatus);
        throw ex;
    }
}

심지어 이건 스프링이 추상화해 둔 PlatformTransactionManager를 써도 그렇습니다.

 

🐜 구현체마다 Transaction 사용법이 다름

  • 일단 DB마다 어떤식으로 Transaction을 시작해야 하는지가 다릅니다.
  • DB는 같다고 해도 JPA, JDBC, JTA 등 어떤 기술을 사용하느냐에 따라서도 Transaction 시작/커밋/롤백이 조금씩 다 다릅니다.

PlatformTransactionManager

모든 Spring의 Transaction은 AOP를 통하던지, Template을 쓰던지 PlatformTransactionManager라는 추상화된 interface를 통합니다.

public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
    void commit(TransactionStatus status) throws TransactionException;
    void rollback(TransactionStatus status) throws TransactionException;
}
  • TransactionDefinition에 Transaction에 관한 설정들을 넣고
  • .getTransaction()을 호출하면 Transaction이 시작합니다. (이미 시작됐으면 기존 Transaction을 가져옵니다.)
  • 그리고 최종적으로 commit이나 rollback을 할 수 있는 구조입니다.

이렇게 하면 기존 Transaction의 문제점인 구현체마다 Transaction 사용법이 다른 문제를 해결할 수 있습니다.


@Transactional의 내부 구조 (AOP 부분)

@Transactional 또한 내부에서는 PlatformTransactionManager를 사용합니다. @Transactional의 내부 구조를 까 보면서 Spring이 Transaction을 어떤 식으로 처리하는지에 대한 이해를 좀 더 높여 보겠습니다! ✌️

Join Point TransactionInterceptor
Pointcut TransactionAttributeSourcePointcut
Advice TransactionAspectSupport

 

Join Point

public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
    public TransactionInterceptor() {}

    public TransactionInterceptor(TransactionManager ptm, TransactionAttributeSource tas) {
        setTransactionManager(ptm);
        setTransactionAttributeSource(tas);
    }

    @Override
    @Nullable
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : 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();
            }
        });
	}
}
  • @TransactionalJoin PointTransactionInterceptor의 구조는 사실 단순합니다.
  • MethodInterceptor를 구현한 invoke에서 targetClass를 가져온 뒤, @TransactionalAdviceTransactionAspectSupport의 invokeWithinTransaction을 호출한게 끝입니다.

 

Point Cut

Pointcut은 굳이 볼 필요 없을 거 같아서 넘어가겠습니다. 😅

 

Advice

사실 핵심인 Advice만 보면 됩니다. 그중에서도 핵심인 invokeWithinTransaction만 살펴보겠습니다.

public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {
    @Nullable
    protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final org.springframework.transaction.interceptor.TransactionAspectSupport.InvocationCallback invocation) throws Throwable {
        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) {
            boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
            boolean hasSuspendingFlowReturnType = isSuspendingFunction && COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName());
            if (isSuspendingFunction && !(invocation instanceof org.springframework.transaction.interceptor.TransactionAspectSupport.CoroutinesInvocationCallback)) {
                throw new IllegalStateException("Coroutines invocation not supported: " + method);
            }
            org.springframework.transaction.interceptor.TransactionAspectSupport.CoroutinesInvocationCallback corInv = (isSuspendingFunction ? (org.springframework.transaction.interceptor.TransactionAspectSupport.CoroutinesInvocationCallback) invocation : null);

            org.springframework.transaction.interceptor.TransactionAspectSupport.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 org.springframework.transaction.interceptor.TransactionAspectSupport.ReactiveTransactionSupport(adapter);
            });

            org.springframework.transaction.interceptor.TransactionAspectSupport.InvocationCallback callback = invocation;
            if (corInv != null) {
                callback = () -> CoroutinesUtils.invokeSuspendingFunction(method, corInv.getTarget(), corInv.getArguments());
            }
            Object result = txSupport.invokeWithinTransaction(method, targetClass, callback, txAttr, (ReactiveTransactionManager) tm);
            if (corInv != null) {
                Publisher<?> pr = (Publisher<?>) result;
                return (hasSuspendingFlowReturnType ? org.springframework.transaction.interceptor.TransactionAspectSupport.KotlinDelegate.asFlow(pr) : org.springframework.transaction.interceptor.TransactionAspectSupport.KotlinDelegate.awaitSingleOrNull(pr, corInv.getContinuation()));
            }
            return result;
        }

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

        if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
            org.springframework.transaction.interceptor.TransactionAspectSupport.TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

            Object retVal;
            try {
                retVal = invocation.proceedWithInvocation();
            } catch (Throwable ex) {
                completeTransactionAfterThrowing(txInfo, ex);
                throw ex;
            } finally {
                cleanupTransactionInfo(txInfo);
            }

            if (retVal != null && vavrPresent && org.springframework.transaction.interceptor.TransactionAspectSupport.VavrDelegate.isVavrTry(retVal)) {
                TransactionStatus status = txInfo.getTransactionStatus();
                if (status != null && txAttr != null) {
                    retVal = org.springframework.transaction.interceptor.TransactionAspectSupport.VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
                }
            }

            commitTransactionAfterReturning(txInfo);
            return retVal;
        } else {
            Object result;
            final org.springframework.transaction.interceptor.TransactionAspectSupport.ThrowableHolder throwableHolder = new org.springframework.transaction.interceptor.TransactionAspectSupport.ThrowableHolder();

            try {
                result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> {
                    org.springframework.transaction.interceptor.TransactionAspectSupport.TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
                    try {
                        Object retVal = invocation.proceedWithInvocation();
                        if (retVal != null && vavrPresent && org.springframework.transaction.interceptor.TransactionAspectSupport.VavrDelegate.isVavrTry(retVal)) {
                            retVal = org.springframework.transaction.interceptor.TransactionAspectSupport.VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
                        }
                        return retVal;
                    } catch (Throwable ex) {
                        if (txAttr.rollbackOn(ex)) {
                            if (ex instanceof RuntimeException) {
                                throw (RuntimeException) ex;
                            } else {
                                throw new org.springframework.transaction.interceptor.TransactionAspectSupport.ThrowableHolderException(ex);
                            }
                        } else {
                            throwableHolder.throwable = ex;
                            return null;
                        }
                    } finally {
                        cleanupTransactionInfo(txInfo);
                    }
                });
            } catch (org.springframework.transaction.interceptor.TransactionAspectSupport.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;
            }

            if (throwableHolder.throwable != null) {
                throw throwableHolder.throwable;
            }
            return result;
        }
    }
}

솔직히 첫인상을 쉽지 않다... 결코 잘 짠 코드라 할 수는 없을 거 같습니다. 하지만 다행히 저희가 주목해야 할 부분은 이 중 일부입니다. 정확히는... 

  1. 어디서 Transaction을 만들고
  2. 어디서 기존 로직을 실행하고
  3. 어디서 rollback을 하고
  4. 어디서 commit을 하는지만 찾으면 됩니다.

 

🐜 1번 Transaction 만드는 부분 == 41번째 줄

TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

여기서는 ptm이라는 변수에 PlatformTransactionManager를 전달해 주고 있습니다. 그럼 createTransactionIfNecessary에서 이 ptm으로 무엇을 할까요?

protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
    @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {

    if (txAttr != null && txAttr.getName() == null) {
        txAttr = new DelegatingTransactionAttribute(txAttr) {
            @Override
            public String getName() {
                return joinpointIdentification;
            }
        };
    }

    TransactionStatus status = null;
    if (txAttr != null) {
        if (tm != null) {
            status = tm.getTransaction(txAttr);
        }
    }
    
    return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

바로 .getTransaction()을 호출하고 있습니다!

 

🐜 2번 기존 로직 실행하는 부분 == 45번째 줄

Object retVal;
try {
    retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
    completeTransactionAfterThrowing(txInfo, ex);
    throw ex;
} finally {
    cleanupTransactionInfo(txInfo);
}

 

🐜 3번 Rollback하는 부분 == 47번째 줄

try {
    retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
    completeTransactionAfterThrowing(txInfo, ex);
    throw ex;
} finally {
    cleanupTransactionInfo(txInfo);
}

여기서 Exception이 발생하면 Rollback을 해야 하는데 Exception이 발생하면 completeTransactionAfterThrowing을 호출합니다. 그럼 이 함수를 한 번 봐 볼까요?

protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
    if (txInfo != null && txInfo.getTransactionStatus() != null) {
        if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
            try {
                txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
            } catch (TransactionSystemException ex2) {
                ex2.initApplicationException(ex);
                throw ex2;
            } catch (RuntimeException | Error ex2) {
                throw ex2;
            }
        } else {
            try {
                txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
            } catch (TransactionSystemException ex2) {
                ex2.initApplicationException(ex);
                throw ex2;
            } catch (RuntimeException | Error ex2) {
                throw ex2;
            }
        }
    }
}
  • 일단 Rollback을 해야 하는지를 확인합니다. txInfo.transactionAttribute.rollbackOn(ex) 부분에서 이 예외가 Rollback 해야 하는 예외인지를 확인합니다.
  • Rollback해야 하는 예외가 맞으면 txInfo.getTransactionManager()를 가져와서 .rollback()을 호출하네요? 이 .getTransactionManager()가 뭘까요?
public PlatformTransactionManager getTransactionManager() {
    Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
    return this.transactionManager;
}

네! 바로 PlatformTransactionManager 였습니다!

 

🐜 4번 Commit하는 부분 == 60번째 줄

commitTransactionAfterReturning(txInfo);

비슷하게 commitTransactionAfterReturning에서 어떤 걸 하는지 보면 됩니다.

protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
    if (txInfo != null && txInfo.getTransactionStatus() != null) {
        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
    }
}

바로... (두구두구) 당연하게도 PlatformTransactionManager.commit()을 호출하고 있습니다.


@Transactional의 내부 구조 (PlatformTransactionManager부분)

@Transactional을 쓰면 내부에서는 결국 PlatformTransactionManager를 호출한다는 걸 확인했습니다. 그렇다면 PlatformTransactionManager 자체는 어떻게 돼 있을까요?

사실 PlatformTransactionManager는 여러 구현체가 있기 때문에 어떤 구현체를 사용하냐에 따라 다릅니다. 하지만 일반적으로 Jdbc를 많이 사용하기 때문에 이 구현체를 한 번 봐 보겠습니다.

public class JdbcTransactionManager extends DataSourceTransactionManager {}

JdbcTransactionManager를 보면 DataSourceTransactionManager를 상속하고 있습니다. 사실 Spring은 Connection 객체를 얻는 방법을 DataSource로 추상화해 놨으니 DataSourceTransactionManager를 상속해서 JdbcTransactionManager를 구현하는 게 올바른 추상화로 느껴집니다. 그럼 DataSourceTransactionManager를 봐 볼까요?

 

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean {}

하지만 DataSourceTransactionManager에는 PlatformTransactionManager의 getTransaction, commit, rollback의 구현이 없고, 해당 구현은 AbstractPlatformTransactionManager에 있습니다. 사실 생각해 보면 PlatformTransactionManager의 구현체라고 해도 어떤 Connection이나 Rollback 방법을 쓸 것인지 등의 차이는 있어도 구현은 대부분 비슷할 가능성이 높습니다. 그래서 AbstractPlatformTransactionManager에서 기본적인 구현 방법을 제공하고, 바꿔 끼우기를 원하는 부분만 상속해서 바꾸는 식으로 돼 있습니다.

예를 들면 DataSourceTransactionManager의 생성자 중 1개는 AbstractPlatformTransactionManagersetNestedTranactionAllowed라는 속성을 바꾸고 있습니다.

public DataSourceTransactionManager() {
    this.enforceReadOnly = false;
    this.setNestedTransactionAllowed(true);
}

반응형