@Transactional Try ~ catch를 적용한 경우의 코드는 다음과 같습니다.
@Transactional
public Long save(Object object) {
Long saveId;
try {
saveId = repository.save(Object);
} catch (RuntimeException re ) {
log.error("save fail, message: {},", re.getMessage());
}
return saveId;
}
문제 발생
@Transactional 안에 try ~ catch로 감싸져 있는 코드가 있었습니다. 문제는 try ~ catch 안에서 익셉션이 발생했을 때 롤백이 동작하지 않는 것이었습니다.
@Transactional에 대해 찾아보게 되었습니다.
@Transactional을 동작시키면 아래의 순서로 동작하게 됩니다.
service레이어에서Exception발생TransactionAspectSupport.java의invokeWithinTransaction함수를 실행invokeWithinTransaction의 try ~ catch에서completeTransactionAfterThrowing실행
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable {
// If the transaction attribute is null, the method is non-transactional.
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
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);
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
....
TransactionAspectSupport.java
completeTransactionAfterThrowing함수 실행txInfo.transactionAttribute.rollbackOn(ex)동작
protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.hasTransaction()) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
if (txInfo.transactionAttribute.rollbackOn(ex)) {
...
구현체 RuleBasedTransactionAttribute.java
- rollbackOn은
if (winner == null)이면super.rollbackOn(ex);동작
@Override
public boolean rollbackOn(Throwable ex) {
if (logger.isTraceEnabled()) {
logger.trace("Applying rules to determine whether transaction should rollback on " + ex);
}
RollbackRuleAttribute winner = null;
int deepest = Integer.MAX_VALUE;
if (this.rollbackRules != null) {
for (RollbackRuleAttribute rule : this.rollbackRules) {
int depth = rule.getDepth(ex);
if (depth >= 0 && depth < deepest) {
deepest = depth;
winner = rule;
}
}
}
if (logger.isTraceEnabled()) {
logger.trace("Winning rollback rule is: " + winner);
}
// User superclass behavior (rollback on unchecked) if no rule matches.
if (winner == null) {
logger.trace("No relevant rollback rule found: applying default rules");
return super.rollbackOn(ex);
}
return !(winner instanceof NoRollbackRuleAttribute);
}
구현체 DefaultTransactionAttribute.java의 rollbackOn을 보면
- RuntimeException
- Error
에 대해 롤백을 처리하는 것을 확인할 수 있습니다.
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
@Transactional은 RuntimeException과 Error를 잡는 것을 확인하였습니다.
그렇다면 왜 @Transactional 안의 try ~ catch에서는 롤백이 되지 않는 것일까요?
그 이유는 서비스 레이어에서 try ~ catch로 예외를 잡을 경우, TransactionAspectSupport.java의 invokeWithinTransaction에서 catch로 잡지 못하게 되어, 롤백이 진행되지 않기 때문입니다.
해결 방법
- try ~ catch로 감싼 후 예외가 발생했을 때 throw로 Unchecked Exception을 발생시킵니다.
@Transactional
public Long save(Object object) {
Long saveId;
try {
saveId = repository.save(Object);
} catch (RuntimeException re ) {
log.error("save fail, message: {},", re.getMessage());
throw new RuntimeException("UnChecked Exception");
}
return saveId;
}
후기
오늘은 트랜잭션에 대해 분석하게 되었습니다. 기본적으로 트랜잭션은 RuntimeException과 Error에 대해 롤백이 진행되며, checkException에 대해서는 catch에서 RuntimeException을 발생시켜 롤백시켜 줄 수 있다는 것을 알 수 있었습니다. 추가로 왜 try ~ catch에서 롤백이 되지 않는지 명확하게 알 수 있는 기회였습니다.
댓글
GitHub 계정으로 로그인하시면 댓글과 질문을 남기실 수 있습니다. 남겨 주신 글은 이 저장소의 Discussions에 쌓입니다.