우아한테크코스/post우테코

[내부코드 파헤치기] Spring Transaction

nauni 2021. 12. 24. 09:57

mvc, jdbc 등을 몇 개 내부코드를 살펴보았지만 아직 스프링 내부의 코드를 보는 것이 그렇게 익숙하지는 않다. Transactional 어노테이션과 관련하여 스프링에서는 트랜잭션이 어떻게 동작하는지 내부를 조금 확인해본다.

 

DataSourceTransactionManager

아래의 코드를 보면 커넥션 맺기부터 시작하여 Transactional 어노테이션이 가지고 있는 여러 옵션들을 셋팅해주는 메소드가 있는 것을 알 수 있다. Transaction마다 커넥션이 새로 생성되는가에 대해서 크루들과 얘기해본 적이 있는데, 학습테스트를 진행한 결과에서는 propagation 설정을 주고 여러 트랜잭션을 호출할 때에도 내부 트랜잭션이 생길 때에도 커넥션이 새로 생성되었다. 그 근거로 이 내부코드에서 Transacion을 실행할 때마다 '커넥션 맺기'가 진행되는 것을 알 수 있다.

// DataSourceTransactionManager
// public class DataSourceTransactionManager extends AbstractPlatformTransactionManager


    @Override
	protected void doBegin(Object transaction, TransactionDefinition definition) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
		Connection con = null;

		try {
			if (!txObject.hasConnectionHolder() ||
					txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
				Connection newCon = obtainDataSource().getConnection(); // 커넥션 맺기
				if (logger.isDebugEnabled()) {
					logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
				}
				txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
			}

			txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
			con = txObject.getConnectionHolder().getConnection(); // 커넥션 맺기

			Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
			txObject.setPreviousIsolationLevel(previousIsolationLevel); // 격리레벨 셋팅
			txObject.setReadOnly(definition.isReadOnly()); // readOnly 셋팅

			// Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
			// so we don't want to do it unnecessarily (for example if we've explicitly
			// configured the connection pool to set it already).
			if (con.getAutoCommit()) {
				txObject.setMustRestoreAutoCommit(true);
				if (logger.isDebugEnabled()) {
					logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
				}
				con.setAutoCommit(false); // autoCommit을 false로 셋팅
			}

			prepareTransactionalConnection(con, definition);
			txObject.getConnectionHolder().setTransactionActive(true);

			int timeout = determineTimeout(definition); // timeout 옵션
			if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
				txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
			}

			// Bind the connection holder to the thread.
			if (txObject.isNewConnectionHolder()) {
				TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
			}
		}

		catch (Throwable ex) {
			if (txObject.isNewConnectionHolder()) {
				DataSourceUtils.releaseConnection(con, obtainDataSource()); // 자원해제
				txObject.setConnectionHolder(null, false);
			}
			throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
		}
	}

AbstractPlatformTransactionManager

위의 DataSourceTransactionManager는 AbstractPlatformTransactionManager를 상속하고 있다. startTransaction 메서드에서 위에서 살펴본 doBegin의 메서드가 사용되는 것을 알 수 있다. 

// AbstractPlatformTransactionManager

	private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction,
			boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) {

		boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
		DefaultTransactionStatus status = newTransactionStatus(
				definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
		doBegin(transaction, definition); // 위의 doBegin 메소드가 사용된는 것을 알 수 있다.
		prepareSynchronization(status, definition);
		return status;
	}

TransactionTemplate

rollback 되고 commit 되는 코드는 어디있을까 하여 찾게되었다. 스프링은 수많은 클래스 파일로 구성되어 있어 아직 전체적으로 어떤 플로우를 거쳐 Transaction이 동작하는지 정확하게 알긴 어려웠다. 하지만 아래 메서드를 확인할 때, try-catch를 통해 rollback, commit을 처리해주는 것을 알 수 있다.

// TransactionTemplate
    
	@Override
	@Nullable
	public <T> T execute(TransactionCallback<T> action) throws TransactionException {
		Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");

		if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
			return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
		}
		else {
			TransactionStatus status = this.transactionManager.getTransaction(this);
			T result;
			try {
				result = action.doInTransaction(status);
			}
			catch (RuntimeException | Error ex) {
				// Transactional code threw application exception -> rollback
				rollbackOnException(status, ex); // 예외가 발생하면 rollback
				throw ex;
			}
			catch (Throwable ex) {
				// Transactional code threw unexpected exception -> rollback
				rollbackOnException(status, ex);
				throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
			}
			this.transactionManager.commit(status); // 아니라면 commit
			return result;
		}
	}

정리

정확하게 AOP가 어떤 방식으로 작동하고, 각각의 클래스가 어떻게 유기적인 순서로 동작하는지 까지는 파악하기 어려웠다. 추후 AOP 공부를 좀 더 해보면서 이런 모호한 부분은 더 이어나갈 수 있다고 생각한다. 내부 코드를 보는 것이 쉽지 않은 일이었는데, 조금씩이라도 내부 동작원리를 코드를 통해 이해해나가 보려고 한다. dependency를 이용해서 스프링과 스프링부트가 제공해주는 기능들을 확인할 수 있는 상태로 사용하는 것이 올바른 사용방식이라고 생각한다. 우아한테크코스에서 이렇게 공부하는 크루들을 보면서 많이 배웠다. 지금까지는 되는 것에 집중했다면 앞으로는 동작원리를 이해하고 사용하는 것으로 발전해 나가야 한다고 생각한다.

출처

- 스프링 내부 코드