기존 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();
}
});
}
}
- @Transactional의 Join Point인 TransactionInterceptor의 구조는 사실 단순합니다.
- MethodInterceptor를 구현한 invoke에서 targetClass를 가져온 뒤, @Transactional의 Advice인 TransactionAspectSupport의 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;
}
}
}
솔직히 첫인상을 쉽지 않다... 결코 잘 짠 코드라 할 수는 없을 거 같습니다. 하지만 다행히 저희가 주목해야 할 부분은 이 중 일부입니다. 정확히는...
- 어디서 Transaction을 만들고
- 어디서 기존 로직을 실행하고
- 어디서 rollback을 하고
- 어디서 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개는 AbstractPlatformTransactionManager의 setNestedTranactionAllowed라는 속성을 바꾸고 있습니다.
public DataSourceTransactionManager() {
this.enforceReadOnly = false;
this.setNestedTransactionAllowed(true);
}
'👨💻 프로그래밍 > 📦 Backend' 카테고리의 다른 글
자주 쓰는 Kotlin Live Template 공유 (0) | 2024.06.02 |
---|---|
Spring에서 DB 예외 처리하는 법 알아보기 (0) | 2024.05.07 |
JDBC, 필요한 만큼만 알아보기 (0) | 2024.05.05 |
도메인 모델 풍부하게 만들기 (1) | 2024.05.04 |
Spring의 @Controller, @Repository, @Service, @Component 등 알아보기 (0) | 2024.05.01 |