Spring Data JPA multi-datasource 글로벌 트랜잭션 사용하기(JtaTransactionManager) #2

ssongkim·2023년 7월 8일
1

Spring

목록 보기
2/2

Overivew

이전 게시글을 통해 하나의 스프링 애플리케이션에서 여러 데이터베이스에 붙어 JPA로 작업하는 방법을 알아보았습니다.

이전 게시글의 문제점은 TCC를 지원하지 않는다는 점이였습니다. 왜냐, 물리적으로 다른 트랜잭션 매니저를 사용하기 때문에 각각의 트랜잭션 매니저는 서로의 작업 처리 결과를 알지 못합니다.
이들을 묶어 처리하고 싶다면 글로벌 트랜잭션을 설정하고 사용해주어야 합니다.

오늘은 Atomikos를 이용해 글로벌 트랜잭션을 구현해보겠습니다.


참고로 2023.07.09 기준으로 Atomikos는 스프링 부트3.0부터 지원하지 않습니다. 스프링 부트 3을 지원하는 별도의 Atomikos 라이브러리가 있는 듯 합니다.
https://stackoverflow.com/questions/75949411/how-to-replace-atomikos-in-distributed-transaction-in-spring-boot-3

JtaTransactionManager는 무엇이고 내부 동작원리에 대해 궁금하다면 해당 링크를 참고합니다.
https://d2.naver.com/helloworld/5812258

공식문서
spring boot starter atomikos application 설정내용 https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties.transaction

1.의존성 설정

implementation 'org.springframework.boot:spring-boot-starter-jta-atomikos'

2.애플리케이션 설정파일 작성

spring:      
  datasource:
    one:
      xa-properties:
        url: jdbc:mysql://localhost:3306/test1?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC&allowPublicKeyRetrieval=true
        user: root
        password: root
      xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
      unique-resource-name: 'one'
      test-query: SELECT 1
      min-pool-size: 5
      max-pool-size: 10
      hibernate:
        ddl-auto: update
        naming:
          implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
          physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

    two:
      xa-properties:
        url: jdbc:mysql://localhost:3307/test2?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC&allowPublicKeyRetrieval=true
        user: root
        password: root
      xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
      unique-resource-name: 'two'
      min-pool-size: 2
      max-pool-size: 2
      hibernate:
        ddl-auto: update

xa-properties 설정 가능 리스트를 알고싶다면 AtomikosDataSourceBean 열어봐서 참고합니다.

@Data
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public class DatabaseProperties {
  private One one;
  private Two two;

  @Data
  public static class One {
    private XaProperties xaProperties;
    private String xaDataSourceClassName;
    private String uniqueResourceName;
    private int maxPoolSize;
    private Hibernate hibernate;

  }

  @Data
  public static class Two {
    private XaProperties xaProperties;
    private String xaDataSourceClassName;
    private String uniqueResourceName;
    private int maxPoolSize;
    private Hibernate hibernate;

  }

  @Data
  public static class XaProperties {
    private String url;
    private String user;
    private String password;
  }

  @Data
  public static class Hibernate {
    private String ddlAuto;
    private Naming naming;

    public static Map<String, Object> propertiesToMap(Hibernate hibernateProperties) {
      Map<String, Object> properties = new HashMap<>();

      properties.put("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName());
      properties.put("javax.persistence.transactionType", "JTA");

      if(hibernateProperties.getDdlAuto() != null) {
        properties.put("hibernate.hbm2ddl.auto", hibernateProperties.getDdlAuto());
      }

      DatabaseProperties.Naming hibernateNaming = hibernateProperties.getNaming();
      if(hibernateNaming != null) {
        if (hibernateNaming.getImplicitStrategy() != null) {
          properties.put("hibernate.implicit_naming_strategy",  hibernateNaming.getImplicitStrategy());
        }
        if (hibernateNaming.getPhysicalStrategy() != null) {
          properties.put("hibernate.physical_naming_strategy", hibernateNaming.getPhysicalStrategy());
        }
      }

      return properties;
    }   }

  @Data
  public static class Naming {
    private String implicitStrategy;
    private String physicalStrategy;
  }
}

설정파일을 객체에 매핑해 사용하기 위해 정의합니다.

3.XaDataSourceConfig 작성

@Configuration
@EnableTransactionManagement
public class XaDataSourceConfig {
  public static final String TRANSACTION_MANAGER_BEAN_NAME = "jtaTransactionManager";

  @Bean
  public UserTransactionManager userTransactionManager() throws SystemException {
    UserTransactionManager userTransactionManager = new UserTransactionManager();
    userTransactionManager.setTransactionTimeout(1000);
    userTransactionManager.setForceShutdown(true);

    return userTransactionManager;
  }

  @Bean
  public UserTransaction userTransaction() throws SystemException {
    var userTransaction = new UserTransactionImp();
    userTransaction.setTransactionTimeout(60000);

    return userTransaction;
  }

  @Primary
  @Bean(name = TRANSACTION_MANAGER_BEAN_NAME)
  public JtaTransactionManager jtaTransactionManager(UserTransactionManager userTransactionManager, UserTransaction userTransaction) {
    JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
    jtaTransactionManager.setTransactionManager(userTransactionManager);
    jtaTransactionManager.setUserTransaction(userTransaction);

    return jtaTransactionManager;
  }
}

글로벌 트랜잭션 매니저를 설정합니다.

4. 각 데이터베이스 별 설정파일 작성

이전 게시글과의 차이점은 트랜잭션 매니저를 별도로 선언해 사용하지 않고 위에서 선언한 공통된 트랜잭션 매니저를 사용한다는 것입니다.

또한 엔티티 매니저 properties에 JTA 관련 내용을 추가했습니다.

주DB

@Configuration
@EnableConfigurationProperties(DatabaseProperties.class)
@EnableJpaRepositories(
    basePackages = {"com.example.javajpamultidatabaseexample.account", "com.example.javajpamultidatabaseexample.board"},
    entityManagerFactoryRef = PersistenceDBOneConfiguration.ENTITY_MANAGER_BEAN_NAME,
    transactionManagerRef = XaDataSourceConfiguration.TRANSACTION_MANAGER_BEAN_NAME)
public class PersistenceDBOneConfiguration {
  public static final String ENTITY_MANAGER_BEAN_NAME = "oneDBEntityManager";
  private static final String DATASOURCE_BEAN_NAME = "oneDataSource";
  private static final String DATASOURCE_PROPERTIES_PREFIX = "spring.datasource.one";
  private static final String HIBERNATE_PROPERTIES = "oneHibernateProperties";

  @Primary
  @Bean(name = ENTITY_MANAGER_BEAN_NAME)
  public LocalContainerEntityManagerFactoryBean entityManager(EntityManagerFactoryBuilder builder, @Qualifier(DATASOURCE_BEAN_NAME) DataSource dataSource,
      @Qualifier(HIBERNATE_PROPERTIES) DatabaseProperties.Hibernate hibernateProperties) {

    return builder.dataSource(dataSource).packages("com.example.javajpamultidatabaseexample.account", "com.example.javajpamultidatabaseexample.board")
        .persistenceUnit(ENTITY_MANAGER_BEAN_NAME)
        .properties(DatabaseProperties.Hibernate.propertiesToMap(hibernateProperties)).build();
  }

  @Bean(name = HIBERNATE_PROPERTIES)
  @ConfigurationProperties(DATASOURCE_PROPERTIES_PREFIX + ".hibernate")
  public DatabaseProperties.Hibernate hibernateProperties() {
    return new DatabaseProperties.Hibernate();
  }

  @Primary
  @Bean(name = DATASOURCE_BEAN_NAME)
  @ConfigurationProperties(prefix = DATASOURCE_PROPERTIES_PREFIX)
  public DataSource dataSource() {
    return new AtomikosDataSourceBean();
  }
}

그 외DB들

@Configuration
@EnableConfigurationProperties(DatabaseProperties.class)
@EnableJpaRepositories(
    basePackages = {"com.example.javajpamultidatabaseexample.school"},
    entityManagerFactoryRef = PersistenceDBTwoConfiguration.ENTITY_MANAGER_BEAN_NAME,
    transactionManagerRef = XaDataSourceConfiguration.TRANSACTION_MANAGER_BEAN_NAME)
public class PersistenceDBTwoConfiguration {
  public static final String ENTITY_MANAGER_BEAN_NAME = "twoDBEntityManager";
  private static final String DATASOURCE_BEAN_NAME = "twoDataSource";
  private static final String DATASOURCE_PROPERTIES_PREFIX = "spring.datasource.two";
  private static final String HIBERNATE_PROPERTIES = "twoHibernateProperties";

  @Bean(name = ENTITY_MANAGER_BEAN_NAME)
  public LocalContainerEntityManagerFactoryBean entityManager(EntityManagerFactoryBuilder builder, @Qualifier(DATASOURCE_BEAN_NAME) DataSource dataSource,
      @Qualifier(HIBERNATE_PROPERTIES) DatabaseProperties.Hibernate hibernateProperties) {

    return builder.dataSource(dataSource).packages("com.example.javajpamultidatabaseexample.school").persistenceUnit(ENTITY_MANAGER_BEAN_NAME)
        .properties(DatabaseProperties.Hibernate.propertiesToMap(hibernateProperties)).build();
  }

  @Bean(name = HIBERNATE_PROPERTIES)
  @ConfigurationProperties(DATASOURCE_PROPERTIES_PREFIX + ".hibernate")
  public DatabaseProperties.Hibernate hibernateProperties() {
    return new DatabaseProperties.Hibernate();
  }

  @Bean(name = DATASOURCE_BEAN_NAME)
  @ConfigurationProperties(prefix = DATASOURCE_PROPERTIES_PREFIX)
  public DataSource dataSource() {
    return new AtomikosDataSourceBean();
  }
}

사용하기

@Override
  @Transactional
  public void test() {
    // db1에 해당하는 엔티티
	// db2에 해당하는 엔티티
  }

이전과 다른점은 모든 데이터베이스가 사용하는 트랜잭션 매니저는 글로벌 트랜잭션 매니저 하나이므로 굳이 지정해주지 않아도 된다는 것입니다.

글로벌 트랜잭션의 단점

내부적으로 Two Phase Commit을 해주어 일관성을 보장해준다는 장점은 좋지만,, 이는 곧 성능저하로 이어집니다.

글로벌 트랜잭션을 사용해야한다면 성능저하 측면에서 괜찮은지 연구해보고 사용해야할 듯 합니다. (저희는 사용하지 않기로 했습니다..!)

profile
鈍筆勝聰✍️

0개의 댓글