본문 바로가기

SPRING

[SPRING] 다중 Database 및 분산 Transaction 설정 예제 ChainedTransactionManager vs JtaTransactionManager

 

Spring boot JPA 다중 database를 설정한 후 여러 datasource에 다중 transaction 설정을 하려고 한다

 

 

먼저 다중 database를 설정해주자

 

다중 Database 설정

yml 파일에 다음과 같이 2개의 디비를 선언해준다

첫번째 database : my_advanced_admin

두번째 database : my_advanced_member

 

spring:
  datasource:
    admin:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: "jdbc:mysql://127.0.0.1:3306/my_advanced_admin?serverTimezone=UTC&characterEncoding=UTF-8"
      username: "root"
      password: "password"
    member:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: "jdbc:mysql://127.0.0.1:3306/my_advanced_member?serverTimezone=UTC&characterEncoding=UTF-8"
      username: "root"
      password: "password!"

 

 

그리고 domain폴더와 repository 구조도 datasource와 같이 구성해주자 

 

 

 

my_advanced_admin 디비의 databaseConfig 설정

package com.my.advancedSpring.common.config.database;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        basePackages = "com.my.advancedSpring.repository.admin",
        entityManagerFactoryRef = "myAdminEntityManager",
        transactionManagerRef = "myAdminTransactionManager"
)
public class MyAdminConfiguration {

    @Bean(name = "myAdminEntityManager")
    @Primary
    public LocalContainerEntityManagerFactoryBean myAdminEntityManager() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(myAdminDataSource());
        em.setPackagesToScan("com.my.advancedSpring.domain.admin");

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setShowSql(true);
        vendorAdapter.setGenerateDdl(true);
        em.setJpaVendorAdapter(vendorAdapter);

        return em;
    }

    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.admin")
    public DataSource myAdminDataSource() {
        return DataSourceBuilder.create().build();
    }

	@Primary
    @Bean(name = "myAdminTransactionManager")
    public PlatformTransactionManager myAdminTransactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(myAdminEntityManager().getObject());
        return transactionManager;
    }
}

 

 

my_advanced_member 디비의 databaseConfig 설정

package com.my.advancedSpring.common.config.database;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        basePackages = "com.my.advancedSpring.repository.member",
        entityManagerFactoryRef = "myUserEntityManager",
        transactionManagerRef = "myUserTransactionManager"
)
public class MyUserConfiguration {
    @Bean(name = "myUserEntityManager")
    public LocalContainerEntityManagerFactoryBean myUserEntityManager() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(myUserDataSource());
        em.setPackagesToScan("com.my.advancedSpring.domain.member");

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setShowSql(true);
        vendorAdapter.setGenerateDdl(true);
        em.setJpaVendorAdapter(vendorAdapter);

        return em;
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.member")
    public DataSource myUserDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "myUserTransactionManager")
    public PlatformTransactionManager myUserTransactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(myUserEntityManager().getObject());
        return transactionManager;
    }
}

 

 

이렇게 하면 다중 데이터베이스 설정이 완료된다

그러나 이 상태면 my_advanced_admin, my_advanced_member 디비 양쪽에 한꺼번에 트랜잭션이 걸리지 않고

@Primary 어노테이션이 붙은 transactionManager를 위주로 트랜잭션이 걸린다

 

즉, 여기서는 my_advanced_admin 디비에만 transaction이 제대로 반영되고 my_advanced_member 디비에는 transaction 속성이 작동하지 않는다

 

해결방법 ! ! !  분산 transaction을 사용한다

 

 

분산 Transaction

distributed transaction은 2개 이상의 네트워크 시스템 간의 트랜잭션이다. 일반적으로 시스템은 트랜잭션 리소스의 역할을 하고, 트랜잭션 매니저는 이러한 리소스에 관련된 모든 동작에 대해 트랜잭션의 생성 및 관리를 담당한다.

분산 트랜잭션은 4가지 ACID(원자성, 일관성, 고립성, 지속성) 속성을 갖추어야 하며, 여기에서 원자성은 일의 단위(UOW)를 위해 'all or nothing' 결과를 보증해야 한다. 여기선 my_advanced_admin디비와 my_advanced_member디비가 분산 트랜잭션의 리소스이다.

 

이제 ChainedTransactionManager 설정을 해주자

 

 

ChainedTransactionManager 설정

먼저, MyAdminConfiguration.java 파일 myAdminTransactionManager() 에 @Primary 속성을 제거해주고

    @Bean(name = "myAdminTransactionManager")
    public PlatformTransactionManager myAdminTransactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(myAdminEntityManager().getObject());
        return transactionManager;
    }

 

 

ChaintedTrasactionConfig 설정을 해주자

package com.my.advancedSpring.common.config.database;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.transaction.ChainedTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class ChainedTransactionConfig {

    @Autowired
    @Bean
    @Primary
    public PlatformTransactionManager chainedTransactionManager(
            @Qualifier("myUserTransactionManager") PlatformTransactionManager myUserTransactionManager,
            @Qualifier("myAdminTransactionManager") PlatformTransactionManager myAdminTransactionManager
    ) {
        return new ChainedTransactionManager(myUserTransactionManager, myAdminTransactionManager);
    }

}

 

이때 중요한게 @Primary 속성을 chainedTransactionManager() 에 붙여주는 것이다.

그래야 chainedTransactionManager() 메소드가 가장 먼저 설정이 되고 하위에 myAdminTransactionManager(), myUserTransactionManager() 가 반영되기 때문이다.

 

그런데 사실 ChainedTransactionManager는 완벽한 Rollback을 보장하지 않는다.

ChainedTransactionManager는 PlatformTransactionManager를 역순으로 반복문을 돌면서 커밋을 하는데 중간에 실패할 경우 아직 커밋 처리하지 않은 transaction들에 대해서만 rollback 처리를 한다고 한다.

 

즉, transaction을 중단시킬 가능성이 가장 큰(예외가 발생할 확률이 가장 큰) PlatformTransactionManager를 마지막에 설정해야 한다. 위 코드에서는 myAdminTransaction이 예외가 발생할 확률이 가장 크다.

 

 

그래서 또 ! ! ! 이에대한 해결방안으로 JtaTransactionManager라는 것이 있다.

 

 

먼저 JtaTransactionManager에서 사용되는 xa에 대해 정리해보면

 

XA

분산 트랜잭션 처리를 위해 X/Open이 제정한 표준 스펙으로 멀티 트랜잭션 관리자와 로컬 리소스 관리자 사이의 인터페이스, 리소스 관리자가 트랜잭션을 처리하기 위해 필요한 것을 규정하고 있다.

2단계 커밋 프로토콜 수행을 통해, 분산된 데이터베이스에서 발생하는 각 트랜잭션을 원자적인 트랜잭션으로 구성할 수 있게 한다. 

 

 

JTA

JTA(Java Transaction API)는 XA 리소스(예: 데이터베이스) 간의 분산 트랜잭션을 처리하는 Java API로 Atomikos, Bitronix, Narayana 와 같은 JTA 오픈소스 인터페이스가 있다.

JtaTransactionManager는 캡슐화된 UserTransaction 단위로 트랜잭션 경계를 구분하고 각 트랜잭션 작업을 정보를 그룹화하여 XA 처리 레이어로 전달한다.

XA 처리 레이어에서는 prepare() 단계에서 각 트랜잭션 작업의 state를 판단하여 모두 이상이 없으면 다음 단계인 커밋을 수행하고 이상이 있는 경우에는 모든 트랜잭션에 대해서 롤백을 수행한다.

 

한문장으로 설명하자면 JTA를 지원하는 자원을 가리키는 XA Resource 인터페이스의 구현체들을 등록한 후, 해당 구현체들에 대해서 전역 transaction을 지원해 주는 것을 말한다.

 

 

정리

ChainedTrasactionManager는 완벽한 rollback을 보장하지 않기때문에 PlatformTransactionManager의 순서를 고려한 등록이 필요하다.

JtaTransactionManager는 완벽한 rollback을 보장하지만 실행속도가 ChainedTrasactionManager에 비해 비교적 느릴 수 있다.

 

 

끄읏 -!