하나의 서비스에서 여러 데이터베이스를 동시에 처리해야 하는 경우가 있다.

이때 단일 DataSource 기반의 @Transactional만으로는 여러 데이터베이스 간의 트랜잭션 일관성을 보장하기 어렵다.

예를 들어, 하나의 함수에서 A 데이터베이스 수정 → B 데이터베이스 수정 중 오류 발생

이런 상황에서 A와 B 데이터베이스를 모두 함께 롤백하려면 글로벌 트랜잭션(JTA) 이 필요하다.

JTA가 필요없는 예시:
•회원 정보 수정 → A DB만 사용
•인증 정보 수정 → B DB만 사용

이처럼 상황에 따라 특정 데이터베이스만 접근하는 구조라면, 트랜잭션 매니저를 동적으로 선택하는 방식을 참고하는 것이 더 적절할 수 있다.
(관련 내용은 아래 링크 참고)
https://thornapp.tistory.com/10

 

하나의 DB에서 트랜잭션 매니저를 동적으로 선택하는 방법 (Spring AOP)

배경 및 문제점운영 중인 시스템에서 다음과 같은 상황이 있었다.• 하나의 물리 DB• 특정 역할에 따른 DB 계정 분리• MyBatis + Spring Transaction 사용하지만 Spring의 기본 @Transactional 은→ 트랜잭션

thornapp.tistory.com

 

아키텍처 구성

• Spring Boot
• Atomikos (JTA Transaction Manager)
• Oracle XA DataSource
• MyBatis
• 다중 DB (Primary / Secondary / Third)

java 코드 예제
@Configuration
public class DataSourceConfig {

    /* =====================================================
     * XA DataSource 설정
     *
     * - AtomikosDataSourceBean은 XA 트랜잭션을 지원하는 DataSource
     * - 각 DataSource는 서로 다른 DB를 바라보지만
     * - JTA TransactionManager에 의해 하나의 글로벌 트랜잭션으로 묶인다
     * ===================================================== */

    /**
     * Primary XA DataSource
     * - 기본으로 사용될 DataSource
     * - @Primary로 지정하여 주입 시 우선 선택
     */
    @Primary
    @Bean(name = "primaryDataSource")
    @ConfigurationProperties("spring.datasource.primary")
    public DataSource primaryDataSource() {
        return new AtomikosDataSourceBean();
    }

    /**
     * Secondary XA DataSource
     */
    @Bean(name = "secondaryDataSource")
    @ConfigurationProperties("spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return new AtomikosDataSourceBean();
    }

    /**
     * Third XA DataSource
     */
    @Bean(name = "thirdDataSource")
    @ConfigurationProperties("spring.datasource.third")
    public DataSource thirdDataSource() {
        return new AtomikosDataSourceBean();
    }

    /* =====================================================
     * JTA Transaction Manager 설정
     *
     * - 여러 XA DataSource를 하나의 글로벌 트랜잭션으로 관리
     * - @Transactional 하나로 다중 DB 트랜잭션을 제어 가능
     * - 내부적으로 2-Phase Commit(2PC) 수행
     * ===================================================== */

    @Bean
    public PlatformTransactionManager transactionManager() throws SystemException {

        // UserTransaction: 트랜잭션의 시작 / 커밋 / 롤백을 제어
        UserTransactionImp userTx = new UserTransactionImp();
        userTx.setTransactionTimeout(30);

        // Atomikos의 핵심 트랜잭션 매니저
        // 여러 XA Resource(DataSource)를 관리
        UserTransactionManager atomikosTxManager = new UserTransactionManager();
        atomikosTxManager.init();

        // Spring에서 사용하는 JTA 트랜잭션 매니저
        // @Transactional 이 이 매니저를 통해 글로벌 트랜잭션을 제어
        return new JtaTransactionManager(userTx, atomikosTxManager);
    }

    /* =====================================================
     * SqlSessionFactory 설정
     *
     * - 각 DataSource마다 별도의 SqlSessionFactory 생성
     * - 하지만 트랜잭션은 모두 JTA(TransactionManager) 하나로 관리됨
     * ===================================================== */

    @Primary
    @Bean(name = "primarySqlSessionFactory")
    public SqlSessionFactory primarySqlSessionFactory(
            @Qualifier("primaryDataSource") DataSource dataSource,
            ApplicationContext context) throws Exception {

        return createSqlSessionFactory(dataSource, context);
    }

    @Bean(name = "secondarySqlSessionFactory")
    public SqlSessionFactory secondarySqlSessionFactory(
            @Qualifier("secondaryDataSource") DataSource dataSource,
            ApplicationContext context) throws Exception {

        return createSqlSessionFactory(dataSource, context);
    }

    @Bean(name = "thirdSqlSessionFactory")
    public SqlSessionFactory thirdSqlSessionFactory(
            @Qualifier("thirdDataSource") DataSource dataSource,
            ApplicationContext context) throws Exception {

        return createSqlSessionFactory(dataSource, context);
    }

    /**
     * SqlSessionFactory 공통 생성 메서드
     *
     * - DataSource만 다르고 MyBatis 설정은 동일
     * - 유지보수를 위해 공통 메서드로 분리
     */
    private SqlSessionFactory createSqlSessionFactory(
            DataSource dataSource,
            ApplicationContext context) throws Exception {

        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setConfigLocation(
                context.getResource("classpath:mybatis/mybatis-config.xml"));
        factoryBean.setMapperLocations(
                context.getResources("classpath:mapper/**/*.xml"));
        return factoryBean.getObject();
    }

    /* =====================================================
     * SqlSessionTemplate 설정
     *
     * - MyBatis에서 실제 SQL 실행에 사용되는 객체
     * - 각각 다른 DB를 바라보지만
     * - 트랜잭션은 JTA에 의해 하나로 묶인다
     * ===================================================== */

    @Primary
    @Bean(name = "primarySqlSessionTemplate")
    public SqlSessionTemplate primarySqlSessionTemplate(
            @Qualifier("primarySqlSessionFactory") SqlSessionFactory factory) {
        return new SqlSessionTemplate(factory);
    }

    @Bean(name = "secondarySqlSessionTemplate")
    public SqlSessionTemplate secondarySqlSessionTemplate(
            @Qualifier("secondarySqlSessionFactory") SqlSessionFactory factory) {
        return new SqlSessionTemplate(factory);
    }

    @Bean(name = "thirdSqlSessionTemplate")
    public SqlSessionTemplate thirdSqlSessionTemplate(
            @Qualifier("thirdSqlSessionFactory") SqlSessionFactory factory) {
        return new SqlSessionTemplate(factory);
    }
}
DB 설정 yml 예제
spring:
  datasource:
    primary:
      unique-resource-name: primary-db
      xa-datasource-class-name: oracle.jdbc.xa.client.OracleXADataSource
      xa-properties:
        URL: jdbc:oracle:thin:@...
        user: primary_user
        password: primary_password
      min-pool-size: 1
      max-pool-size: 50
      borrow-connection-timeout: 30
      maintenance-interval: 60
      max-lifetime: 3600
      max-idleTime: 60

    secondary:
      unique-resource-name: secondary-db
      xa-datasource-class-name: oracle.jdbc.xa.client.OracleXADataSource
      xa-properties:
        URL: jdbc:oracle:thin:@...
        user: secondary_user
        password: secondary_password
      min-pool-size: 1
      max-pool-size: 50
      borrow-connection-timeout: 30
      maintenance-interval: 60
      max-lifetime: 3600
      max-idleTime: 60
      
    third:
      unique-resource-name: third-db
      xa-datasource-class-name: oracle.jdbc.xa.client.OracleXADataSource
      xa-properties:
        URL: jdbc:oracle:thin:@...
        user: third_user
        password: third_password
      min-pool-size: 1
      max-pool-size: 50
      borrow-connection-timeout: 30
      maintenance-interval: 60
      max-lifetime: 3600
      max-idleTime: 60

lifetime 등 설정값은 찾아보시고 본인 프로젝트에 맞게 변경하시기 바랍니다.

적용예시
@Transactional
public void process() {
    primaryMapper.insertA();
    secondaryMapper.insertB();
    thirdMapper.insertC();
    // 하나라도 실패하면 전체 롤백
}

 

주의사항

• unique-resource-name 중복 금지
• 커스텀한 로컬 트랜잭션과 혼용해서 사용 X
• 성능 오버헤드 고려
• XA 지원 DB만 가능

JTA방식 VS AOP기반 트랜잭션 동적 선택방식
구분 Atomikos + JTA AOP 기반 트랜잭션 선택
트랜잭션 범위 여러 DB를 하나의 글로벌 트랜잭션으로 묶음 DB별 로컬 트랜잭션
정합성 보장 ✅ 완전 보장 (2PC) ❌ 직접 처리 필요
실패 시 처리 자동 전체 롤백 보상 트랜잭션 필요
구현 난이도 높음 중~낮음
성능 상대적으로 느림 빠름
DB 제약 XA 지원 DB 필요 제약 거의 없음
추천 사용처 금융, 정산, 주문, 결제 조회 위주, 비핵심 데이터

+ Recent posts