트랜잭션 관리


스프링의 빈은 기본적으로 싱글톤이다. 즉, 하나의 서비스 객체 인스턴스가 여러 스레드에서 공유되어 사용된다. 하지만 트랜잭션은 스레드마다 독립적으로 관리되어야 한다. 그래서 ThreadLocal로 관리한다.

TransactionSynchronizationManager

스프링은 TransactionSynchronizationManager 클래스를 통해 트랜잭션 리소스와 상태를 관리한다. 이 클래스는 ThreadLocal 변수를 사용하여 각 스레드별로 독립적인 트랜잭션 컨텍스트를 유지한다.

// TransactionSynchronizationManager의 일부 (간략화됨)
public abstract class TransactionSynchronizationManager {
    // 리소스를 스레드별로 저장하는 ThreadLocal 맵
    private static final ThreadLocal<Map<Object, Object>> resources = 
            new NamedThreadLocal<>("Transactional resources");
    
    // 트랜잭션 동기화 활성화 상태
    private static final ThreadLocal<Boolean> actualTransactionActive = 
            new NamedThreadLocal<>("Actual transaction active");
    
    // 현재 트랜잭션 이름
    private static final ThreadLocal<String> currentTransactionName = 
            new NamedThreadLocal<>("Current transaction name");
    
    // 읽기 전용 플래그
    private static final ThreadLocal<Boolean> currentTransactionReadOnly = 
            new NamedThreadLocal<>("Current transaction read-only status");
    
    // 격리 수준
    private static final ThreadLocal<Integer> currentTransactionIsolationLevel = 
            new NamedThreadLocal<>("Current transaction isolation level");
    
    // 트랜잭션 동기화 콜백 객체
    private static final ThreadLocal<List<TransactionSynchronization>> synchronizations = 
            new NamedThreadLocal<>("Transaction synchronizations");
    
    // 리소스 바인딩 메서드
    public static void bindResource(Object key, Object value) {
        Map<Object, Object> map = resources.get();
        if (map == null) {
            map = new HashMap<>();
            resources.set(map);
        }
        map.put(key, value);
    }
    
    // 리소스 조회 메서드
    public static Object getResource(Object key) {
        Map<Object, Object> map = resources.get();
        if (map == null) {
            return null;
        }
        return map.get(key);
    }
    
    // 리소스 제거 메서드
    public static Object unbindResource(Object key) {
        Map<Object, Object> map = resources.get();
        if (map == null) {
            return null;
        }
        Object value = map.remove(key);
        if (map.isEmpty()) {
            resources.remove();
        }
        return value;
    }
    
    // 기타 메서드들...
}

실제 동작 흐름

  1. 트랜잭션 시작:

    // DataSourceTransactionManager의 doBegin 메서드 (간략화됨)
    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
        Connection con = null;
    
        try {
            // 데이터소스에서 연결 획득
            con = obtainDataSourceConnection();
            // 자동 커밋 해제 (트랜잭션 시작)
            con.setAutoCommit(false);
    
            // 트랜잭션 격리 수준 설정
            Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
            txObject.setPreviousIsolationLevel(previousIsolationLevel);
    
            // 연결을 트랜잭션 리소스로 ThreadLocal에 바인딩
            TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
            // 기타 트랜잭션 속성 설정...
            TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
            TransactionSynchronizationManager.setCurrentTransactionName(definition.getName());
            TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(definition.getIsolationLevel());
            TransactionSynchronizationManager.initSynchronization();
        }
        catch (Throwable ex) {
            // 실패시 정리 로직...
        }
    }
    
  2. 트랜잭션 중 리소스 활용:

    // JdbcTemplate 같은 데이터 액세스 기술은 현재 스레드에 바인딩된 커넥션을 사용
    // DataSourceUtils.getConnection의 일부 (간략화됨)
    public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
        ConnectionHolder conHolder = (ConnectionHolder) 
            TransactionSynchronizationManager.getResource(dataSource);
    
        if (conHolder != null && conHolder.hasConnection()) {
            // ThreadLocal에서 현재 트랜잭션 커넥션을 가져옴
            return conHolder.getConnection();
        }
        else {
            // 트랜잭션 없으면 새 커넥션 획득
            Connection con = fetchConnection(dataSource);
            // ...
            return con;
        }
    }
    
  3. 트랜잭션 커밋/롤백:

    // DataSourceTransactionManager의 doCommit 메서드 (간략화됨)
    @Override
    protected void doCommit(DefaultTransactionStatus status) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
        Connection con = txObject.getConnectionHolder().getConnection();
        try {
            con.commit();
        }
        catch (SQLException ex) {
            throw new TransactionSystemException("Could not commit JDBC transaction", ex);
        }
    }
    
    // DataSourceTransactionManager의 doRollback 메서드 (간략화됨)
    @Override
    protected void doRollback(DefaultTransactionStatus status) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
        Connection con = txObject.getConnectionHolder().getConnection();
        try {
            con.rollback();
        }
        catch (SQLException ex) {
            throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
        }
    }
    
  4. 트랜잭션 종료 후 정리:

    // DataSourceTransactionManager의 doCleanupAfterCompletion 메서드 (간략화됨)
    @Override
    protected void doCleanupAfterCompletion(Object transaction) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
    
        // ThreadLocal에서 리소스 언바인딩
        TransactionSynchronizationManager.unbindResource(getDataSource());
    
        // 동기화 리소스 정리
        TransactionSynchronizationManager.clearSynchronization();
    
        // 커넥션 정리 (원래 상태로 복원)
        Connection con = txObject.getConnectionHolder().getConnection();
        try {
            // 자동 커밋 원복
            con.setAutoCommit(true);
            // 격리 수준 원복
            DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel());
            // 커넥션 반환
            DataSourceUtils.releaseConnection(con, getDataSource());
        }
        catch (Throwable ex) {
            // 예외 처리...
        }
    }
    

TL;DR

  1. ThreadLocal 저장소: TransactionSynchronizationManager는 ThreadLocal을 사용하여 각 스레드별로 독립적인 트랜잭션 컨텍스트와 리소스를 관리한다. 이를 통해 싱글톤 서비스 객체가 동시에 여러 트랜잭션을 처리할 수 있다.

  2. 리소스 바인딩: 트랜잭션 매니저는 트랜잭션 시작 시 데이터베이스 커넥션 같은 리소스를 ThreadLocal에 바인딩한다.

  3. 리소스 참조: 트랜잭션 내에서 데이터 액세스 코드는 ThreadLocal에서 바인딩된 리소스를 가져와 사용한다.

  4. 리소스 정리: 트랜잭션 완료 후 ThreadLocal에서 리소스를 제거하고 원래 상태로 복원한다.

이러한 메커니즘을 통해 스프링은 싱글톤 빈을 사용하면서도 트랜잭션 관리를 각 요청마다 독립적으로 수행할 수 있다. 또한 이 방식은 트랜잭션 전파 속성을 구현하는 데도 필수적이며, 하나의 스레드 내에서 여러 트랜잭션 메서드 호출 간의 관계를 관리하는 데 사용된다.

트랜잭션 전파 속성


트랜잭션 전파 속성(Transaction Propagation)은 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 새로운 트랜잭션 메서드가 호출되었을 때 어떻게 동작할지를 결정하는 정책. @Transactional 애노테이션의 propagation 값으로 지정

  1. REQUIRED (기본값)

    • 현재 트랜잭션이 있으면 그 트랜잭션을 사용한다.
    • 없으면 새 트랜잭션을 생성한다.
    @Transactional(propagation = Propagation.REQUIRED)
    public void serviceMethod() { ... }
    
  2. REQUIRES_NEW

    • 항상 새로운 트랜잭션을 생성한다.
    • 기존 트랜잭션이 있으면 일시 중단시키고 새 트랜잭션을 실행한 후 기존 트랜잭션을 계속한다.
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void mustHaveNewTransaction() { ... }
    
  3. SUPPORTS

    • 현재 트랜잭션이 있으면 그 트랜잭션을 사용한다.
    • 없으면 트랜잭션 없이 실행한다.
    @Transactional(propagation = Propagation.SUPPORTS)
    public void canWorkWithOrWithoutTx() { ... }
    
  4. NOT_SUPPORTED

    • 트랜잭션 없이 실행한다.
    • 현재 트랜잭션이 있으면 일시 중단시킨다.
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void mustRunWithoutTransaction() { ... }
    
  5. MANDATORY

    • 현재 트랜잭션이 있어야만 실행한다.
    • 없으면 예외를 발생시킨다.
    @Transactional(propagation = Propagation.MANDATORY)
    public void mustRunInExistingTransaction() { ... }
    
  6. NEVER

    • 트랜잭션 없이 실행한다.
    • 현재 트랜잭션이 있으면 예외를 발생시킨다.
    @Transactional(propagation = Propagation.NEVER)
    public void mustNeverRunInTransaction() { ... }
    
  7. NESTED

    • 현재 트랜잭션이 있으면 중첩 트랜잭션을 생성한다.
    • 중첩 트랜잭션은 부모 트랜잭션의 일부로 커밋되지만, 독립적으로 롤백될 수 있다.
    • 없으면 REQUIRED처럼 새 트랜잭션을 생성한다.
    @Transactional(propagation = Propagation.NESTED)
    public void runInNestedTransaction() { ... }
    

TL;DR

이러한 전파 속성을 통해 복잡한 트랜잭션 시나리오를 선언적으로 처리할 수 있음. 예를 들어, 하나의 서비스 메서드가 여러 다른 트랜잭션 메서드를 호출할 때 각 메서드의 트랜잭션 동작을 세밀하게 제어할 수 있다