티스토리 뷰

이 글에서는 Spring Boot 3에서 분산 데이터베이스 트랜잭션 처리 방법을 설명합니다. Spring Framework 공식 문서를 기준으로 분산 트랜잭션의 문제점과 해결 방안을 기술합니다.

소개 · 배경

현대 애플리케이션에서는 여러 데이터베이스에 걸쳐 데이터를 처리해야 하는 경우가 빈번합니다. Spring Framework 공식 트랜잭션 문서에 따르면, 단일 데이터베이스에 대한 트랜잭션은 PlatformTransactionManager를 통해 관리되며, ACID 원칙을 보장합니다.

 

분산 데이터베이스 환경에서는 여러 데이터베이스에 걸쳐 트랜잭션을 관리해야 합니다. Spring Framework 공식 트랜잭션 문서에 따르면, 각 데이터베이스는 독립적인 트랜잭션 매니저를 가지며, 이들 간의 트랜잭션을 조율하는 메커니즘이 필요합니다. 분산 트랜잭션 처리가 필요한 이유는 여러 데이터베이스에 걸친 작업의 원자성을 보장하기 위해서입니다.

Spring 트랜잭션 기본 정의

PlatformTransactionManager

PlatformTransactionManager는 Spring Framework API Javadoc에 정의된 인터페이스로, 트랜잭션의 시작, 커밋, 롤백을 관리하는 역할을 담당합니다. Spring Framework 공식 트랜잭션 문서에 따르면, 이 인터페이스는 트랜잭션 관리를 추상화하여 다양한 트랜잭션 관리 방식을 지원합니다.

 

PlatformTransactionManager는 단일 데이터베이스에 대한 트랜잭션을 관리합니다. getTransaction() 메서드는 트랜잭션을 시작하고 TransactionStatus를 반환하며, commit()과 rollback() 메서드는 각각 트랜잭션을 커밋하거나 롤백합니다. 이러한 설계는 단일 데이터소스에 대한 트랜잭션 관리를 표준화하기 위해 제공됩니다.

트랜잭션 전파

Spring Framework 공식 트랜잭션 문서에 따르면, 트랜잭션 전파(Transaction Propagation)는 트랜잭션이 중첩된 메서드 호출에서 어떻게 동작할지를 결정합니다. PROPAGATION_REQUIRED는 기존 트랜잭션이 있으면 참여하고 없으면 새로운 트랜잭션을 생성하며, PROPAGATION_REQUIRES_NEW는 항상 새로운 트랜잭션을 생성합니다.

 

트랜잭션 전파는 단일 데이터소스 환경에서 메서드 간 트랜잭션 경계를 관리하기 위해 설계되었습니다. 그러나 분산 데이터베이스 환경에서는 각 데이터베이스가 독립적인 트랜잭션 매니저를 가지므로, 트랜잭션 전파만으로는 분산 트랜잭션을 처리할 수 없습니다.

 

분산 DB 트랜잭션 문제 분석

분산 트랜잭션의 한계

Spring Framework 공식 트랜잭션 문서에 따르면, 표준 PlatformTransactionManager는 단일 데이터소스에 대한 트랜잭션만 관리합니다. 여러 데이터베이스에 걸쳐 트랜잭션을 처리하려면 각 데이터베이스마다 독립적인 트랜잭션이 생성되며, 이들 간의 원자성을 보장할 수 없습니다.

 

예를 들어, 데이터베이스 A와 데이터베이스 B에 각각 데이터를 저장하는 작업이 있다고 가정합니다. 각 데이터베이스는 독립적인 트랜잭션으로 처리되므로, 데이터베이스 A의 작업이 성공하고 데이터베이스 B의 작업이 실패하면 데이터베이스 A의 변경사항은 커밋되고 데이터베이스 B의 변경사항은 롤백됩니다. 이는 분산 트랜잭션의 원자성을 위반합니다.

2-Phase Commit의 필요성

분산 트랜잭션의 원자성을 보장하기 위해서는 2-Phase Commit(2PC) 프로토콜이 필요합니다. 2PC는 모든 참여자가 준비(Prepare) 단계에서 성공한 후에만 커밋(Commit) 단계를 수행하는 프로토콜입니다. 이를 통해 모든 참여자가 성공하거나 모두 롤백되도록 보장할 수 있습니다.

 

Spring Framework는 2PC를 지원하기 위해 JTA(Java Transaction API)를 제공합니다. JTA는 분산 트랜잭션을 관리하기 위한 표준 API이며, JtaTransactionManager를 통해 JTA 트랜잭션 매니저와 연동할 수 있습니다.

 

공식 대안: ChainedTransactionManager, JtaTransactionManager

ChainedTransactionManager

ChainedTransactionManager는 Spring Framework가 제공하는 클래스로, 여러 트랜잭션 매니저를 체인으로 연결하여 순차적으로 커밋하는 방식입니다. Spring Framework 공식 트랜잭션 문서에 따르면, 이 클래스는 여러 데이터소스에 대한 트랜잭션을 순차적으로 관리합니다.

 

ChainedTransactionManager는 2PC를 완전히 지원하지 않습니다. 각 트랜잭션 매니저를 순차적으로 커밋하므로, 중간에 실패하면 이미 커밋된 트랜잭션을 롤백할 수 없습니다. 따라서 완전한 원자성을 보장하지는 않지만, 대부분의 실무 환경에서 충분히 사용할 수 있는 수준의 보장을 제공합니다.

 

Spring Framework가 ChainedTransactionManager를 제공하는 이유는 JTA 없이도 분산 트랜잭션을 처리할 수 있는 간단한 방법을 제공하기 위해서입니다. JTA는 추가 인프라(예: Atomikos, Bitronix)가 필요하지만, ChainedTransactionManager는 추가 인프라 없이 사용할 수 있습니다.

JtaTransactionManager

JtaTransactionManager는 Spring Framework API Javadoc에 정의된 클래스로, JTA를 사용하여 분산 트랜잭션을 관리합니다. Spring Framework 공식 트랜잭션 문서에 따르면, 이 클래스는 JTA 트랜잭션 매니저와 연동하여 2PC를 지원합니다.

 

JtaTransactionManager는 완전한 분산 트랜잭션을 지원합니다. JTA 트랜잭션 매니저가 2PC 프로토콜을 수행하여 모든 참여자의 원자성을 보장합니다. 그러나 JTA 트랜잭션 매니저는 별도의 인프라(예: Java EE 애플리케이션 서버 또는 독립형 JTA 구현체)가 필요합니다.

 

Spring Framework가 JtaTransactionManager를 제공하는 이유는 표준 JTA API를 통해 완전한 분산 트랜잭션을 지원하기 위해서입니다. JTA는 Java EE 표준이므로 다양한 환경에서 일관된 방식으로 분산 트랜잭션을 처리할 수 있습니다.

 

 

실무 코드 설명

ChainedTransactionManager Bean 설정

ChainedTransactionManager를 사용하여 여러 데이터소스에 대한 트랜잭션을 관리하는 설정 예시입니다:

@Configuration
public class TransactionConfig {

    @Bean
    public PlatformTransactionManager transactionManager(
            DataSourceTransactionManager dataSource1TransactionManager,
            DataSourceTransactionManager dataSource2TransactionManager) {
        return new ChainedTransactionManager(
            dataSource1TransactionManager,
            dataSource2TransactionManager
        );
    }

    @Bean
    public DataSourceTransactionManager dataSource1TransactionManager(
            @Qualifier("dataSource1") DataSource dataSource1) {
        return new DataSourceTransactionManager(dataSource1);
    }

    @Bean
    public DataSourceTransactionManager dataSource2TransactionManager(
            @Qualifier("dataSource2") DataSource dataSource2) {
        return new DataSourceTransactionManager(dataSource2);
    }
}

 

코드 설명:

ChainedTransactionManager는 여러 PlatformTransactionManager를 생성자에 전달받아 체인으로 연결합니다. transactionManager 빈은 ChainedTransactionManager를 반환하며, 이는 Spring의 기본 트랜잭션 매니저로 사용됩니다.

 

Spring Framework 공식 트랜잭션 문서에 따르면, ChainedTransactionManager는 트랜잭션 시작 시 모든 트랜잭션 매니저에 대해 트랜잭션을 시작하고, 커밋 시 역순으로 커밋하며, 롤백 시 정순으로 롤백합니다. 이를 통해 여러 데이터소스에 대한 트랜잭션을 순차적으로 관리합니다.

 

실무에서 사용한 ChainedTransactionManager Bean 설정 코드입니다. 

@Configuration
public class ChainedTxConfig {
	
	/**
	 * By Using the ChainedTransactionManager, you can manage a task using multiple transactions which SEPARATELY access dataSources(which are DIFFERENT dataSources).
	 * It processes transaction managers in order and commit or rollback in reverse. the PlatformTransactionManager rollbacked other transactions if it encounter an Exception(failed).
	 * !! So, you should examine which transaction manager is most likely to fail and place it last order. !!
	 * ChainedTransactionManager does not guarantee that all transactions are rollbacked when one is failed.
	 * */
	
    @SuppressWarnings("deprecation")
	@Bean(name = "chainedTransactionManager_RollbackAll_IfAccTxMgFailed")
    public PlatformTransactionManager chainedTransactionManager1(@Qualifier("mgrTransactionManager") PlatformTransactionManager mgrTransactionManager
    														, @Qualifier("accTransactionManager") PlatformTransactionManager accTransactionManager) {
        return new ChainedTransactionManager(mgrTransactionManager, accTransactionManager);
    }
    
    @SuppressWarnings("deprecation")
	@Bean(name = "chainedTransactionManager_RollbackAll_IfMgrTxMgFailed")
    public PlatformTransactionManager chainedTransactionManager2(@Qualifier("mgrTransactionManager") PlatformTransactionManager mgrTransactionManager
    														, @Qualifier("accTransactionManager") PlatformTransactionManager accTransactionManager) {
        return new ChainedTransactionManager(accTransactionManager, mgrTransactionManager);
    }
}



@Transactional 예시

@Transactional 어노테이션을 사용하여 분산 트랜잭션을 처리하는 예시입니다:

@Service
public class DistributedService {

    @Autowired
    private DataSource1Repository dataSource1Repository;

    @Autowired
    private DataSource2Repository dataSource2Repository;

    @Transactional
    public void processDistributedTransaction(Entity1 entity1, Entity2 entity2) {
        dataSource1Repository.save(entity1);
        dataSource2Repository.save(entity2);
    }
}

코드 설명:

@Transactional 어노테이션이 적용된 메서드는 ChainedTransactionManager를 통해 여러 데이터소스에 대한 트랜잭션을 관리합니다. dataSource1Repository.save()와 dataSource2Repository.save()는 각각 다른 데이터소스에 데이터를 저장하지만, 하나의 트랜잭션으로 묶여 처리됩니다.

 

Spring Framework 공식 트랜잭션 문서에 따르면, ChainedTransactionManager는 트랜잭션 시작 시 모든 데이터소스에 대해 트랜잭션을 시작하고, 메서드가 정상적으로 완료되면 역순으로 커밋합니다. 예외가 발생하면 정순으로 롤백하지만, 이미 커밋된 트랜잭션은 롤백할 수 없으므로 완전한 원자성을 보장하지는 않습니다.

 

실무에서 사용한 @Transactional 사용해  분산 트랜잭션을 처리한 코드입니다.

@Transactional(value = "chainedTransactionManager_RollbackAll_IfAccTxMgFailed")
public void executeTask(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    /***/
}

 

추가 고려사항 및 팁

ChainedTransactionManager의 한계

ChainedTransactionManager는 완전한 분산 트랜잭션을 보장하지 않습니다. 중간에 실패하면 이미 커밋된 트랜잭션을 롤백할 수 없으므로, 보상 트랜잭션(Compensating Transaction)을 고려해야 합니다. 보상 트랜잭션은 이미 커밋된 작업을 취소하는 별도의 트랜잭션을 의미합니다.

 

실무에서는 비즈니스 요구사항을 고려하여 ChainedTransactionManager의 한계를 수용할지, 아니면 JtaTransactionManager를 사용하여 완전한 분산 트랜잭션을 구현할지 결정해야 합니다. 대부분의 경우 ChainedTransactionManager로 충분하지만, 완전한 원자성이 필수적인 경우에는 JtaTransactionManager를 사용해야 합니다.

JtaTransactionManager 사용 시 고려사항

JtaTransactionManager를 사용하려면 JTA 트랜잭션 매니저가 필요합니다. Java EE 애플리케이션 서버를 사용하는 경우 서버가 제공하는 JTA 트랜잭션 매니저를 사용할 수 있으며, 독립형 애플리케이션의 경우 Atomikos나 Bitronix 같은 독립형 JTA 구현체를 사용해야 합니다.

 

JTA 트랜잭션 매니저는 2PC 프로토콜을 수행하므로 성능 오버헤드가 발생할 수 있습니다. 또한 JTA 트랜잭션 매니저 자체가 복잡한 인프라이므로, 운영 및 관리 비용이 증가할 수 있습니다. 따라서 완전한 분산 트랜잭션이 반드시 필요한 경우에만 사용하는 것이 권장됩니다.

트랜잭션 순서 고려

ChainedTransactionManager를 사용할 때는 트랜잭션 매니저의 순서를 고려해야 합니다. 커밋은 역순으로, 롤백은 정순으로 수행되므로, 중요한 데이터소스를 먼저 처리하고 덜 중요한 데이터소스를 나중에 처리하는 것이 일반적입니다. 이를 통해 중요한 데이터소스의 트랜잭션이 먼저 커밋되어 롤백 가능성을 최소화할 수 있습니다.

 

맺음말 및 요약

이 글에서는 Spring Boot 3에서 분산 데이터베이스 트랜잭션 처리 방법을 설명했습니다. Spring Framework 공식 트랜잭션 문서에 따르면, 분산 트랜잭션 처리를 위해서는 ChainedTransactionManager 또는 JtaTransactionManager를 사용할 수 있습니다.

 

핵심 체크 포인트는 다음과 같습니다. ChainedTransactionManager는 JTA 없이도 분산 트랜잭션을 처리할 수 있는 간단한 방법을 제공하지만, 완전한 원자성을 보장하지는 않습니다. JtaTransactionManager는 완전한 분산 트랜잭션을 지원하지만, 추가 인프라가 필요합니다. 비즈니스 요구사항을 고려하여 적절한 방식을 선택해야 합니다.

 

분산 트랜잭션 처리는 복잡한 주제이므로, 실제 적용 시에는 성능, 일관성, 복구 전략 등을 종합적으로 고려하여 설계해야 합니다. Spring Framework가 제공하는 다양한 옵션을 이해하고 적절히 활용하여 안정적이고 효율적인 분산 트랜잭션 시스템을 구축할 수 있습니다.