코드 저장소.

스프링에서 사용되는 @Transaction의 작동 원리. 본문

백엔드/Spring

스프링에서 사용되는 @Transaction의 작동 원리.

slown 2025. 2. 6. 18:01

목차

1.트랜잭션?

2.스프링 내부에 있는 트랜잭션 구조 

 


1.트랜잭션?

우선 트랜잭션은 데이터베이스(DB)에서 하나의 논리적 작업 단위(작업 묶음)를 의미하며, 모두 성공하거나(Commit) 실패하면(Rollback) 원래 상태로 되돌리는(Atomic) 연산을 의미합니다. 그리고 트랜잭션을 나타내는 특징은 다음과 같습니다.

 

트랜잭션은 ACID 원칙을 따라야 한다.

특성설명

Atomicity (원자성) 트랜잭션이 모두 성공하거나 모두 실패해야 한다.
Consistency (일관성) 트랜잭션 수행 전후 데이터의 무결성이 보장되어야 한다.
Isolation (격리성) 동시에 여러 트랜잭션이 실행될 때, 서로 간섭하지 않아야 한다.
Durability (지속성) 트랜잭션이 성공적으로 완료되면, 결과가 영구적으로 저장되어야 한다.

 

2.스프링 내부에 있는 트랜잭션 구조 

스프링을 사용을 하면서 트랜잭션 어노테이션이 어떻게 작동을 하는지를 알고 싶어서 내부 코드를 보기로 했습니다. 우선 트랜잭션 어노테이션은 Aop기반으로 작동이 됩니다. 그 후 해당 어노테이션이 있는 부분을 확인되면 다이나믹 프록시 객체를 생성을 해서 스프링에 있는 트랜잭션객체로 보내게 되는데 해당 객체의 구조는 다음과 같이 구성이 되어있습니다.

 

상속도가 복잡하게 되어있지만 우리가  봐야되는 부분은 DataSourceTransactionManager이다. 해당 코드에 있는 doBegin,doCommit 메서드에 있습니다. 

 

@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());

			// 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);
			}

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

			int timeout = determineTimeout(definition);
			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);
		}
	}

 

위의 긴 코드는 doBegin메서드입니다. 해당 메서드는 다음과 같이 작동합니다.

 

DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;

-> transaction 객체를 DataSourceTransactionObject 타입으로 변환하여 트랜잭션 관리에 필요한 정보를 가져온다.

 

if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) {

Connection newCon = obtainDataSource().getConnection(); txObject.setConnectionHolder(new ConnectionHolder(newCon), true);

}

 

-> 현재 트랜잭션 객체가 커넥션을 보유하고 있지 않거나, 트랜잭션과 동기화되지 않은 경우 새로운 데이터베이스 연결을 가져온다.

-> 새로운 ConnectionHolder를 생성하여 트랜잭션 객체에 설정한다.

 

txObject.getConnectionHolder().setSynchronizedWithTransaction(true); con = txObject.getConnectionHolder().getConnection();

 

-> 현재 커넥션을 트랜잭션과 동기화(SynchronizedWithTransaction = true 설정)한다.

-> txObject에서 JDBC Connection 객체를 가져옴.

 

Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); txObject.setPreviousIsolationLevel(previousIsolationLevel); txObject.setReadOnly(definition.isReadOnly());

 

-> prepareConnectionForTransaction()를 호출하여 트랜잭션 격리 수준을 설정한다.

-> 격리 수준을 이전 값과 비교하여 적절한 설정을 적용한다.

-> 트랜잭션이 readOnly인지 확인하고 해당 값을 저장.

 

if (con.getAutoCommit()) {

   txObject.setMustRestoreAutoCommit(true); con.setAutoCommit(false);

}

 

-> JDBC의 자동 커밋(Auto Commit)을 해제해야 하는 경우, 이를 설정

-> setAutoCommit(false)를 호출하여 트랜잭션을 수동 커밋 모드로 변경.

 

prepareTransactionalConnection(con, definition);

txObject.getConnectionHolder().setTransactionActive(true);

int timeout = determineTimeout(definition);

 

if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {       

    txObject.getConnectionHolder().setTimeoutInSeconds(timeout);

}

 

-> prepareTransactionalConnection()을 호출하여 트랜잭션에 필요한 추가적인 설정을 적용.

-> 트랜잭션을 활성화하고(setTransactionActive(true)), 트랜잭션 타임아웃을 설정.

 

if (txObject.isNewConnectionHolder()) {

    TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());

}

 

-> 트랜잭션 객체가 새로운 커넥션을 가지고 있다면, TransactionSynchronizationManager.bindResource()를 호출하여 현재 스레드에 바인딩.

 

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);

}

 

-> 트랜잭션 시작 중 예외 발생 시, 커넥션을 정리하고 CannotCreateTransactionException을 던진다.

 

그다음은 doCommit 메서드입니다.

@Override
	protected void doCommit(DefaultTransactionStatus status) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
		Connection con = txObject.getConnectionHolder().getConnection();
		if (status.isDebug()) {
			logger.debug("Committing JDBC transaction on Connection [" + con + "]");
		}
		try {
			con.commit();
		}
		catch (SQLException ex) {
			throw translateException("JDBC commit", ex);
		}
	}

 

위의 코드는 다음과 같습니다.

 

DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();

Connection con = txObject.getConnectionHolder().getConnection();

 

-> transaction 객체를 DataSourceTransactionObject 타입으로 변환하여 트랜잭션 관리에 필요한 정보를 가져온다.

-> 커낵션홀더에서 커낵션을 한다.

con.commit() 

-> 커밋을 한다. 

 

이 뒤로는 작성된 JDBC코드를 통해서 트랜잭션이 작동이 됩니다.

 

위의 내용을 정리하자면 다음과 같습니다.

  1. Spring @Transaction 어노테이션은 AOP기반이다.
  2. 해당 어노테이션이 있는 부분을 확인을 하고 다이나믹 프록시를 생성
  3. 해당 프록시는 Spring내부에 있는 DataSourceTransactionManager를 실행
  4. 위의  객체 내에 있는 doBegin,doCommit 메서드를 실행
  5. 어노테이션에 있는 JDBC코드를 작동해서 트랜잭션을 실행한다.