본문 바로가기
우아한테크코스 4기/레벨2

[Spring] Transaction 추상화, 동기화

by 나는후니 2022. 4. 27.

스프링은 트랜잭션을 시작(가져오기)하고, 커밋하고, 롤백하는 기능을 추상화하였습니다.

추상화 덕분에 우리는 스프링 트랜잭션을 쉽게 관리할 수 있는데요. 오늘은 스프링 트랜잭션 추상화에 대해 간단히 알아보겠습니다.

public interface PlatformTransactionManager extends TransactionManager {

    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}
  1. 구현체에 의존이 낮아짐

필수 트랜잭션을 위해 사용하는 PlatformTransactionManager 인터페이스 덕분에 필요하다면 트랜잭션을 쉽게 모킹, 스터빙할 수 있게 됐습니다. 즉, 구현체에 직접 의존하는 정도가 매우 낮아진 것이죠.

 

만약 트랜잭션을 추상화한 인터페이스가 존재하지 않는다면 사용하는 data access 전략에 따라 트랜잭션을 가져오고, 커밋하고, 롤백하는 데 많은 코드의 수정이 필요하다고 생각하면 될 것 같습니다.

실제로 각 데이터 접근 기술들은 위 인터페이스를 상속받아 구현체를 생성하고 있습니다. 스프링에서 이 인터페이스를 IoC 컨테이너에 올려 의존성으로 관리하고 있어 우리는 정말 쉽게 트랜잭션을 관리할 수 있습니다.

 

  1. 언체크드 익셉션 발생

트랜잭션 내에서 발생하는 예외는 굉장히 심각한 것들이 많습니다. 하지만 위 인터페이스에서 던지는 TransactionException은 unchecked Exception이기 때문에 개발자가 해당 예외를 catch할 것인지 handling 할 것인지 의사결정 할 수 있습니다.

 

  1. 트랜잭션 동기화

getTransaction은 새로운 트랜잭션을 생성하여 가져오기도 하지만, 만약 적합한 트랜잭션이 현재 콜스택에 존재한다면 해당 트랜잭션을 가져오는 메서드입니다. 즉, Transaction Status가 실행 쓰레드와 연관 있다는 뜻이죠.

 

스프링에서 가장 추천하는 동기화 방식은 템플릿 기반의 영속성 통합 api나 orm api 중 트랜잭션을 인식하는 팩터리 빈을 사용하는 방법이 있습니다. JDBC의 DataSourceUtils, JPA의 EntityManagerFactoryUtils, Hibernate의 SessionFactoryUtils 클래스를 사용하면 connection이 없을 경우 connection을 생성하여

TransactionSynchronizationManager에 등록하고, connection이 존재하는 경우 해당 connection을 가져다 사용합니다.

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
        Assert.notNull(dataSource, "No DataSource specified");

// TransactionSynchronizationManager에서 가져다 씀
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
            conHolder.requested();
            if (!conHolder.hasConnection()) {
                logger.debug("Fetching resumed JDBC Connection from DataSource");
                conHolder.setConnection(fetchConnection(dataSource));
            }
            return conHolder.getConnection();
        }
  //...
  // TransactionSynchronizationManager에 등록
  TransactionSynchronizationManager.registerSynchronization(
                        new ConnectionSynchronization(holderToUse, dataSource));
                holderToUse.setSynchronizedWithTransaction(true);
                if (holderToUse != conHolder) {
                    TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
                }

TransactionSynchronizationManager는 내부적으로 ThreadLocal을 사용하여 구현되어 있어 동일한 쓰레드에 대한 트랜잭션에만 접근이 가능합니다.

 

일반적으로 각 api의 도우미 클래스를 사용하기 때문에 내부적으로 위에서 언급한 클래스를 이용하여 connection을 가져오고 트랜잭션을 실행합니다. 그래서 우리는 아무것도 모르고 @Transactional을 달아 트랜잭션을 사용하고 있던 거죠..

정리

정리하자면 스프링이 트랜잭션을 위한 모든 준비를 해두었기 때문에 우리는 애노테이션으로 트랜잭션을 열고 닫을 수 있습니다.

 

https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction-strategies
https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-resource-synchronization
org.springframework.jdbc.datasource.DataSourceUtils
org.springframework.transaction.support.TransactionSynchronizationManager