알아두면 좋은것

왜 이게 롤백 되는거지!?!!?!

큼큼이 2020. 9. 9. 21:04

woowabros.github.io/experience/2019/01/29/exception-in-transaction.html

 

응? 이게 왜 롤백되는거지? - 우아한형제들 기술 블로그

이 글은 얼마 전 에러로그 하나에 대한 호기심과 의문으로 시작해서 스프링의 트랜잭션 내에서 예외가 어떻게 처리되는지를 이해하기 위해 삽질을 해본 경험을 토대로 쓰여졌습니다. 스프링의

woowabros.github.io

 

일을 하는데 분명히 Exception 을 잡았는데 왜 롤백을 하는거지..?!?! transactional 을 걸고 있긴 한데

try { 

} catch(e : RunException) {
	log.error("EXCEPTION 발생! ${e.message}");
}

로 로그만 찍고 상위의 코드는 계속 수행하려고 했다....

근데 뭐지? 왜지? 롤백이 된다....ㅋㅋㅋ

 

그렇게 검색을해보니 나랑 같은 상황이였던 블로그를 보게 되었고

나도 기억을 하기 위해서 정리해둬야지

 

더보기
  • <5> 문제의 코드부분입니다. try/catch 없이 RuntimeExceptio이 던져지면서 트랜잭션 완료처리가 시작됩니다.
  • <6> RuntimeException 때문에 트랜잭션을 롤백할지 결정하는 규칙을 적용한다네요. 그런데, 딱히 지정된 규칙이 없어서 디폴트 규칙으로 가는군요.
  • <7> 디폴트라더니 급기야 롤백하기로 결정했나봅니다… 참여한 트랜잭션 실패를 선언하고 rollback-only 마킹을 합니다. 여기까지가 예외가 발생한 내부 트랜잭션의 완료처리였습니다.
  • <8> 안에서 발생한 예외를 최초 트랜잭션 메서드에서 잡았습니다.
  • <9> 최초 트랜잭션 메서드가 완료처리를 시작합니다
  • <10> 앞에서 예외는 잡았고 별 문제 없어 보이는 듯하여 최종커밋을 하려고합니다. 그런데 정작 커밋하려는 순간 roll-back only가 마킹되어 있다고 롤백을 해버립니다!

우아한 형제들 블로그에서 발췌한 부분 입니다.

 

1. 트랜젝션이 완료 되는 처리를 하는 곳을 따라가본다 (TransactionAspectSupport.java:543)

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.transactionAttribute.rollbackOn(ex) 의 내부를 봐야하는데

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

내부는 Error 이거나 RunTimeException 의 일부이면 트랜잭션을 롤백이 필요하다고 본다는 것이다... (.....헉....ㅡ_ㅡ...)

그럼 롤백을 하는 이 내부를 들어가보면 txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());

private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
	try {
		boolean unexpectedRollback = unexpected;
		try {
			triggerBeforeCompletion(status);
			if (status.hasSavepoint()) {
				if (status.isDebug()) {
					logger.debug("Rolling back transaction to savepoint");
				}
				status.rollbackToHeldSavepoint();
			}
			else if (status.isNewTransaction()) {
				if (status.isDebug()) {
					logger.debug("Initiating transaction rollback");
				}
				doRollback(status);
			}
			else {
				// Participating in larger transaction
				if (status.hasTransaction()) {
					if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
						if (status.isDebug()) {
							logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
						}
						doSetRollbackOnly(status);
					}
					else {
						if (status.isDebug()) {
							logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
						}
					}
				}
				else {
					logger.debug("Should roll back transaction but cannot - no transaction available");
				}
				// Unexpected rollback only matters here if we're asked to fail early
				if (!isFailEarlyOnGlobalRollbackOnly()) {
					unexpectedRollback = false;
				}
			}
		}
		catch (RuntimeException | Error ex) {
			triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
			throw ex;
		}

		triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);

		// Raise UnexpectedRollbackException if we had a global rollback-only marker
		if (unexpectedRollback) {
			throw new UnexpectedRollbackException(
					"Transaction rolled back because it has been marked as rollback-only");
		}
	}
	finally {
		cleanupAfterCompletion(status);
	}
}

-------------- 해당하는 부분만 가지고 오면 

else {
	// Participating in larger transaction
    if (status.hasTransaction()) {
		if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
			if (status.isDebug()) {
				logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
			}
			doSetRollbackOnly(status);
		}
		else {
			if (status.isDebug()) {
				logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
		}
	}
}

 

1. 트랜젝션을 가지고 있으면

2. localRollBackOnly / globalRollbackOnParicipationFailure 을 검사한다.

이 두 값은 디폴트로 localRollBackOnly = false  ////// globalRollbackOnParicipationFailure=true

이라서 여기서 doSetRollbackOnly 에 롤백을 해야한다고 마킹이 된다.

그리고 쭉쭉 내려가다보면

if (unexpectedRollback) {
	throw new UnexpectedRollbackException("Transaction rolled back because it has been marked as rollback-only");
}

롤백이 마크되서 롤백이된다.

 

근데 다시 생각해보면 잡을꺼면 왜 트랜젝션을 걸었을까?

하하하....