Posts MyBatis-Spring 트랜잭션 관리
Post
Cancel

MyBatis-Spring 트랜잭션 관리

해당 포스팅에서 mybatis, mybatis-spring 사용법을 간단히 살펴보았다.
이번에는 개인적으로 가장 궁금했던 mybatis-spring에서 트랜잭션을 다루는 부분이 내부적으로 어떻게 동작하는지 살펴보려고 한다.
(예제 코드는 훑어보기 편에서 사용된 코드)

들어가기 전 : 예제 코드 다시 살펴보기

  • SqlSession의 구현체로 SqlSessionTemplate 주입해서 사용하는 경우, sqlSession.insert 호출하면 auto-commit됨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MybatisSpringMemberQueryServiceImpl implements MemberQueryService {

  private final SqlSession sqlSession;

  public MybatisSpringMemberQueryServiceImpl(SqlSession sqlSession) {
    this.sqlSession = sqlSession;
  }

  @Override
  public void createMember(String id, String name) {
    Member newMember = new Member(id, name);
    sqlSession.insert("member.save", newMember);
    sqlSessionTemplate.insert("memberHistory.save", MemberHistory.forJoin(newMember));
  }
}
  • 반면 위와 동일하게 SqlSessionTemplate 주입해서 사용하더라도, 아래 코드는 sqlSession.insert 호출하면 auto-commit 되지 않고 transactionManager.commit 시점에서 커밋됨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MybatisSpringMemberQueryServiceImpl implements MemberQueryService {

    private final SqlSession sqlSession;
    private final PlatformTransactionManager transactionManager;

    public MybatisSpringMemberQueryServiceImpl(SqlSession sqlSession, PlatformTransactionManager transactionManager) {
        this.sqlSession = sqlSession;
        this.transactionManager = transactionManager;
    }

    @Override
    public void createMember(String id, String name) {
      TransactionStatus txStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
      Member newMember = new Member(id, name);

      sqlSession.insert("member.save", newMember);
      sqlSession.insert("memberHistory.save", MemberHistory.forJoin(newMember));

      transactionManager.commit(txStatus);
    }

}

이게 어떻게 가능한 것인지 파악하기 위해 아래 순서로 내부 흐름을 살펴볼 예정이다.

  1. transactionManager.getTransaction
  2. sqlSession.insert // 첫번째 insert

1. transactionManager.getTransaction

내부적으로는 다양한 조건들에 의해 더 복잡하게 분기처리 되지만, 위 예제 코드가 어떻게 동작하는지의 흐름을 파악하는 정도로만 시퀀스 다이어그램을 그려봤다. (처음이라 제대로 그린것인지는 잘 모르겠지만 …)
transactionManager.getTransaction 호출시 흐름을 내가 이해한대로 정리하면,

  • 추상 클래스인 AbstractPlatformTransactionManager는 실제 구현체인 DataSourceTransactionManager에게 트랜잭션을 다루기 위한 객체를 만들어달라고 요청한다.
  • 동일한 트랜잭션 내부에서는 하나의 커넥션이 사용되어야 하기 때문에, 그러한 커넥션을 관리할 ConnectionHolder가 필요하다(트랜잭션 동기화). 따라서, 트랜잭션을 다루기 위한 객체는 ConnectionHolder를 필요로하고 이를 얻기 위해 TransactionSynchronizationManager에게 요청한다.
  • AbstractPlatformTransactionManager는 얻은 트랜잭션 객체(DataSourceTransactionObject)가 이미 처리하고 있는 트랜잭션이 존재하는지 확인한다.
  • 없는 경우, 트랜잭션 처리를 시작하기 위한 준비를 한다.
    (트랜잭션 객체에 ConnectionHolder 세팅, ConnectionHolder에 트랜잭션 동기화 여부 세팅, ConnectionHolder에서 관리하는 Connection에 auto-commit false 세팅, TransactionSynchronizationManager에 ConnectionHolder 세팅되도록 등)
  • 트랜잭션 및 트랜잭션 동기화와 관련된 속성들을 TransactionSynchronizationManager쪽에 세팅한다.

    즉, transactionManager.getTransaction“기존에 처리되고 있는 트랜잭션이 있는지 확인(1)하고, 없으면 트랜잭션 처리를 위해 준비(2)하는 과정” 이라고 생각하면 될 것 같다.

image

  • 참고 : org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
protected void doBegin(Object transaction, TransactionDefinition definition) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject)transaction;
    Connection con = null;

    try {
        if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
            Connection newCon = this.obtainDataSource().getConnection();
            if (this.logger.isDebugEnabled()) {
                this.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());
        if (con.getAutoCommit()) {
            txObject.setMustRestoreAutoCommit(true);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
            }

            con.setAutoCommit(false);
        }

        this.prepareTransactionalConnection(con, definition);
        txObject.getConnectionHolder().setTransactionActive(true);
        int timeout = this.determineTimeout(definition);
        if (timeout != -1) {
            txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
        }

        if (txObject.isNewConnectionHolder()) {
            TransactionSynchronizationManager.bindResource(this.obtainDataSource(), txObject.getConnectionHolder());
        }

    } catch (Throwable var7) {
        if (txObject.isNewConnectionHolder()) {
            DataSourceUtils.releaseConnection(con, this.obtainDataSource());
            txObject.setConnectionHolder((ConnectionHolder)null, false);
        }

        throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", var7);
    }
}
  • 참고 : org.springframework.transaction.support.AbstractPlatformTransactionManager#prepareSynchronization
1
2
3
4
5
6
7
8
9
protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) {
    if (status.isNewSynchronization()) {
        TransactionSynchronizationManager.setActualTransactionActive(status.hasTransaction());
        TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(definition.getIsolationLevel() != -1 ? definition.getIsolationLevel() : null);
        TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
        TransactionSynchronizationManager.setCurrentTransactionName(definition.getName());
        TransactionSynchronizationManager.initSynchronization();
    }
}

2. 첫번째 sqlSession.insert

  • TransactionSynchronizationManager에게 트랜잭션 동기화가 활성화 되어있는지 확인.
  • 활성화되어 있으면 동기화에 필요한 SqlSessionHolder가 있는지 확인
  • SqlSessionHolder가 없으면 생성하여 TransactionSynchronizationManager에 바인딩
  • DB에 질의하기 위해 얻어온 SqlSession의 구현체 DefaultSqlSession를 통해 DB에 질의
    • 질의시 1번 단계(transactionManager.getTransaction)에서 세팅한 ConnectionHolder의 Connection이 사용된다.
  • 트랜잭션 동기화중인지 확인뒤, 동기화 중이지 않으면 질의한 것 바로 commit 후 SqlSession.close()
  • 동기화 중이면, commit 하지않고 동기화에 사용되는 SqlSessionHolder의 SqlSession을 release

  • 요약하면,
    • (1) 트랜잭션 동기화중인지 확인후 동기화를 위한 SqlSessionHolder 등 준비
    • (2) 동기화 중이면, DB에 질의 후 SqlSession을 commit & close하지 않고, SqlSession을 release

image

  • SqlSession의 구현체로 사용되는 SqlSessionTemplate의 내부에서 this.sqlSessionProxy = (SqlSession)Proxy.newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSession.class}, new SqlSessionInterceptor());
This post is licensed under CC BY 4.0 by the author.

JDBC / MyBatis / MyBatis-Spring 훑어보기

프록시를 통해 API 호출할 때 지연되는 이슈