Home ChainedTransactionManager는 왜 Deprecated 됐을까?
Post
Cancel

ChainedTransactionManager는 왜 Deprecated 됐을까?

글을 작성하게 된 계기


분산 트랜잭션(Distributed Transaction) 학습 과정에서 ChainedTransactionManager 에 대해 알게 된 내용을 정리하기 위해 글을 작성하게 되었습니다.

A distributed transaction is a database transaction in which two or more network hosts are involved. Usually, hosts provide transactional resources, while a transaction manager creates and manages a global transaction that encompasses all operations against such resources.





1. ChainedTransactionManager란?


ChainedTransactionManager는 트랜잭션의 생성, 커밋/롤백조정 하는 PlatformTransactionManager의 구현체입니다.

1
2
3
4
5
6
/**
 * The configured instances will start transactions in the order given and commit/rollback in <em>reverse</em> order,
 * which means the {@link PlatformTransactionManager} most likely to break the transaction should be the <em>last</em>
 * in the list configured. A {@link PlatformTransactionManager} throwing an exception during 커밋 will automatically
 * cause the remaining transaction managers to roll back instead of committing.
 * */





이는 각 데이터베이스의 트랜잭션을 담당하는 로컬 트랜잭션 매니저(Local TransactionManager) 들을 체인(Chain) 을 통해 이어줍니다. 이를 통해 여러 데이터베이스별개의 트랜잭션연결된 트랜잭션으로 만들어 관리할 수 있습니다.

image





단, 이는 트랜잭션 매니저들을 단순히 연결 한 것일 뿐, 이를 통해 하나의 트랜잭션 을 만들 수는 없습니다. 즉, 서로 다른 데이터베이스의 트랜잭션들순서 만 고려해 연결했을 뿐, 전체 작업의 일관성 이 보장되지 않는 것이죠. 당연히 각 데이터베이스는 서로 다른 트랜잭션 컨텍스트를 가지고 있기 때문에 ACID가 보장되지 않겠죠?

image

A-B-C 가 하나의 체인으로 이어져 있을 때, C가 실패하면 A, B도 실패로 처리해야 하지만, A, B는 성공할 수 있습니다. A, B, C는 모두 다른 데이터베이스를 바라보고 있기 때문에 각 트랜잭션의 문맥이 다르기 때문입니다.





체인에는 순서 가 존재하며, 이는 매우 중요합니다. 커밋과 롤백은 체이닝한 역순 으로 실행되기 때문에 순서를 잘못 등록하면 커밋, 롤백 단계에서 예기치 못한 결과가 발생할 수 있습니다.

image





ChainedTransactionManager는 로컬 트랜잭션 매니저체이닝 한다고 했는데요, 따라서 이 순서를 스프링 빈을 등록하는 과정에서 지정할 수 있습니다. 아래와 같이요. 코드로 보니까 조금 더 와닿죠?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class ChainedTransactionConfiguration
{
    @Bean
    @Primary
    public PlatformTransactionManager chainedTransactionManager(
        @Qualifier("sourceTransactionManager") 
        PlatformTransactionManager sourceTransactionManager,
        
        @Qualifier("targetTransactionManager") 
        PlatformTransactionManager targetTransactionManager,

        @Qualifier("externalSourceTransactionManager")
        PlatformTransactionManager externalSourceTransactionManager
    ) {
        // 등록 순서
        return new ChainedTransactionManager(
            sourceTransactionManager,            // 1
            targetTransactionManager,            // 2
            externalSourceTransactionManager     // 3
        );
    }
}

이에 대해서는 뒤에서 상세히 다루기 때문에 간단히 언급만 하고 넘어가도록 하겠습니다.





꽤 괜찮은 기능을 제공하는 것 같지만 이는 현재 Deprecated 되었습니다. 왜 ChainedTransactionManager가 Deprecated 되었는지 이해하기 위해서는 다중 데이터베이스 를 사용할 때의 트랜잭션 동작 과정 을 알아야 하는데, 이를 먼저 살펴 보겠습니다.

1
2
@Deprecated
public class ChainedTransactionManager implements PlatformTransactionManager { /**/ }







2. 다중 데이터베이스의 트랜잭션


한 애플리케이션에서 여러 개의 데이터베이스를 바라본다고 가정해 보겠습니다. 이 경우, 하나의 작업(트랜잭션)에 여러 개의 데이터베이스 가 참여할 수 있습니다.

image





모든 작업이 성공해 커밋이 되면 문제가 없지만, 작업 중 하나라도 실패 가 발생하면, 데이터 일관성을 위해 이때까지 진행한 모든 작업을 역순으로 복구 시켜야 합니다. 이전에 성공한 작업까지도요.

image





그런데 우리는 다중 데이터베이스를 사용하기 때문에 데이터베이스 레벨에서 이전에 커밋된 작업을 복구시킬 방법이 없습니다. 서로 다른 데이터베이스 이므로 작업 컨텍스트 가 다르니까요. ChainedTransactionManager는 단순히 순서만 고려해 이를 연결 만 해준다고 했죠?

image





정리해보면 ChainedTransactionManager는 각 데이터베이스의 별개의 트랜잭션을 연결 한 것일 뿐, 하나의 트랜잭션이 아니기 때문에, 도중에 에러가 발생해도 이전에 커밋된 데이터를 롤백할 수 없는 것입니다.

image





이제 왜 Deprecated 된 지 감이 조금 오실 텐데요, 내부 구현을 보며 앞에 설명한 내용을 확인해 보겠습니다.

Using two transaction managers based on AbstractPlatformTransactionManager causes the first transaction manager to handle all synchronizations regardless of their resource origin (primary and secondary transactional resources). If the second transaction manager commit fails, then already all synchronizations are processed and there’s no way to recover. Therefore, we’re going to deprecate ChainedTransactionManager and the entire org.springframework.data.transaction that hosts support classes for multi-transactions.






3. 내부 코드 살펴보기


ChainedTransactionManager가 Deprecated 된 이유는 작업이 실패했을 때, 부분 롤백을 할 방법이 없기 때문 입니다. 이로 인해 데이터의 정합성이 깨질 수 있는데, 이는 commit/rollback 메서드의 내부 구현을 보면 알 수 있습니다. 물론 이 외에도 롤백 처리의 복잡성, 제한된 확장성, 성능 저하 등의 이유도 있고요. 이에 대해서는 별도로 학습해 보실 것을 권장드립니다.

  1. 롤백 처리의 복잡성
  2. 제한된 확장성
  3. 성능 저하





3-1. 커밋

커밋은 reverse 메서드로 TransactionManager를 역순으로 정렬 한 후, 작업을 진행합니다. 위에서 ChainedTransactionManager는 작업을 단순히 체이닝 시켜준다고 이야기 했었는데요, 즉, 체이닝으로 연결된 각 로컬 트랜잭션 매니저들을 순회하며 순서대로 커밋만 진행하는 것입니다.

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
49
50
51
52
53
@Deprecated
public class ChainedTransactionManager implements PlatformTransactionManager {
    
    ......

    public void commit(TransactionStatus status) throws TransactionException {
        
        ......
        
        // 1. 트랜잭션 매니저 역순 정렬
        Iterator var6 = this.reverse(this.transactionManagers).iterator();

        // 2. 반복문을 돌며 커밋 진행
        while(var6.hasNext()) {
            PlatformTransactionManager transactionManager = (PlatformTransactionManager)var6.next();
            if (commit) {
                try {
                    multiTransactionStatus.commit(transactionManager);
                } catch (Exception var9) {
                    commit = false;
                    commitException = var9;
                    commitExceptionTransactionManager = transactionManager;
                }
            } else {
                // 예외 발생 시, 롤백
                try {
                    multiTransactionStatus.rollback(transactionManager);
                } catch (Exception var10) {
                    logger.warn("Rollback exception (after commit) (" + transactionManager + ") " + var10.getMessage(), var10);
                }
            }
        }

        if (multiTransactionStatus.isNewSynchonization()) {
            this.synchronizationManager.clearSynchronization();
        }

        if (commitException != null) {
            boolean firstTransactionManagerFailed = commitExceptionTransactionManager == this.getLastTransactionManager();
            int transactionState = firstTransactionManagerFailed ? 2 : 3;
            throw new HeuristicCompletionException(transactionState, commitException);
        }
    }
    
    ......

    // 트랜잭션 매니저 역순 정렬
    private <T> Iterable<T> reverse(Collection<T> collection) {
        List<T> list = new ArrayList(collection);
        Collections.reverse(list);
        return list;
    }
}

커밋을 역순으로 진행하는 이유는 중간에 실패가 발생할 경우를 대비하기 위함입니다. 이는 트랜잭션의 전파 속성(상위/하위 트랜잭션)을 떠올리면 이해하기 쉽습니다.





이 과정에서 커밋이 실패하면 롤백을 시도 하지만, 이미 다른 데이터베이스에 한 번 반영된 커밋을 다시 돌릴 수는 없습니다. 즉, 순서대로 실행만 시켜주는 것이죠. 따라서 이 과정에서 부분 롤백 실패 가 발생할 수 있습니다.

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
@Deprecated
public class ChainedTransactionManager implements PlatformTransactionManager {
    
    ......

    public void commit(TransactionStatus status) throws TransactionException {
        
        ......

        public void commit (TransactionStatus status) throws TransactionException {
        
            ......

            while (var6.hasNext()) {
                PlatformTransactionManager transactionManager = (PlatformTransactionManager) var6.next();
                if (commit) {
                    ......
                } else {
                    // 예외 발생 시, 롤백
                    try {
                        multiTransactionStatus.rollback(transactionManager);
                    } catch (Exception var10) {
                        logger.warn("Rollback exception (after commit) (" + transactionManager + ") " + var10.getMessage(), var10);
                    }
                }
            }
            ......
        }
        ......
    }
}






3-2. 롤백

롤백도 트랜잭션 매니저를 역순으로 정렬 한 후, 작업을 처리합니다. 커밋과 마찬가지로 이미 다른 데이터베이스에 한 번 반영된 커밋을 다시 돌릴 수는 없습니다. 마찬가지로 이도 순서대로 실행만 할 뿐이죠. 따라서 이 과정에서도 부분 롤백 실패 가 발생할 수 있습니다.

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
@Deprecated
public class ChainedTransactionManager implements PlatformTransactionManager {
    
    ......

    public void rollback(TransactionStatus status) throws TransactionException {
        
        ......
        
        // 1. 트랜잭션 매니저 역순 정렬
        Iterator var5 = this.reverse(this.transactionManagers).iterator();

        // 2. 반복문을 돌며 롤백 진행
        while(var5.hasNext()) {
            PlatformTransactionManager transactionManager = (PlatformTransactionManager)var5.next();

            try {
                multiTransactionStatus.rollback(transactionManager);
            } catch (Exception var8) {
                if (rollbackException == null) {
                    rollbackException = var8;
                    rollbackExceptionTransactionManager = transactionManager;
                } else {
                    logger.warn("Rollback exception (" + transactionManager + ") " + var8.getMessage(), var8);
                }
            }
        }

        if (multiTransactionStatus.isNewSynchonization()) {
            this.synchronizationManager.clearSynchronization();
        }

        if (rollbackException != null) {
            throw new UnexpectedRollbackException("Rollback exception, originated at (" + rollbackExceptionTransactionManager + ") " + rollbackException.getMessage(), rollbackException);
        }
    }
    
    ......

}





정리해보면 ChainedTransactionManager는 commit/rollback 내부에서 로컬 트랜잭션을 체이닝 해 연결된 트랜잭션으로 만듭니다. 하지만 부분 롤백에서 실패 할 수 있기에, 즉, 전체 데이터의 일관성은 보장해주지 않기 때문에 ChainedTransactionManager가 Deprecated 된 것입니다.

Deprecated된 정확한 배경을 보기 위해서는 ChainedTransactionManager Issue를 참조해주세요.







4. 대안


이를 위한 대안으로 JtaTransactionManager 사용, XA 트랜잭션(Distributed Transactions), Saga 패턴 등이 존재합니다. 이에 대해서는 별도의 포스팅을 할 예정이기에 간단히 언급만 하고 넘어가도록 하겠습니다.

  1. JtaTransactionManager
  2. XA 트랜잭션(Distributed Transactions)
  3. Saga 패턴







5. 정리


ChainedTransactionManager는 여러 데이터베이스의 작업을 체인으로 이어주지만, 이어진 작업이 모든 데이터베이스의 데이터 정합성을 맞출 수 없기 때문에 Deprecated 되었습니다. 롤백 처리의 복잡성, 제한된 확장성, 성능 저하 등의 이유도 있고요. 나머지 이유에 대해서는 한 번 학습해 보시길 권장드립니다.

  1. 롤백 처리의 복잡성
  2. 제한된 확장성
  3. 성능 저하

This post is licensed under CC BY 4.0 by the author.

메시지 유실은 언제 발생할 수 있을까?

Retry 패턴을 적용할 때, 어떤 점을 고려해야 할까?