Multi-Transaction

  1. Java Multi-Transaction 관리
  • 테이블 구조
  • 테스트 방법
  • application.yaml 설정
  • 각각 DataSource Config 설정
  • CASE 분별
  • 해결 방안
  • ChainedTransactionManager 사용하기
  • Java Multi-Transaction 관리

    Spring 프로젝트에서 여러 스키마를 이용해 관리 하는 경우 이다.
    이 경우 여러 해당 스키마에 맞게 각각 DataSource 및 transaction 를 관리 해야 되는 상황이 발생된다.

    하지만 각각 스키마에 따라 TransactionManager 관리 하게 되고 독립적으로 Transaction 발생 되면서 하나 Thread 내에서 예기치 않는 문제가 발생 된다.

    예를들어 하나의 비지니스 로직이 동시에 각각 다른 스키마에 존재하는 테이블 DB 에 Insert 및 Update 발생되고

    이 후 Exception 발생시 하나의 Transaction 처럼 commit 및 rollback 이 발생 되지 않아 문제가 발생된다.

    한번 실험 해보자.

    테이블 구조

    2.PNG

    member 테이블은 ‘TEST’ 스키마 이고 role 테이블은 ‘TEST2’ 스키마에 적용된 상태이다.

    테스트 방법

    1.PNG

    해당 Spring Server 에 호출 하게 되면 먼저 TransactionController 에서 라우팅하게 되고 이후 TransactionService 에 전달하게 됩니다.

    전달된 TransactionService 에서 먼저 ‘TEST’ 스키마에 있는 Member 테이블 Insert 하기 위해

    MemberService 에 전달 되고

    ‘TEST2’ 스키마에 있는 Role 테이블 Insert 하기 위해 RoleService 에 전달 하게 된다.

    현재 TransactionManager 경우

    1. MainTransactionManager
    2. SubTransactionManger

    이렇게 두가지가 있다.

    이미지에서 보면 최종적으로 3번 ‘TEST2’ 스키마에서 ‘Role’ 테이블에 Insert 후 일부러 Exception 발생 해서
    ‘TEST’ 스키마에서 ‘Member’ 테이블 Insert 된것이 rollback 되는지 확인 하고자 한다. (Propagation: REQUIRED 경우)

    Propagation 종류 :

    • REQUIRED : 기본 설정, 진행중인 transaction이 있으면 참여, 없으면 새로 생성
    • SUPPORTS : 진행중인 transaction이 있으면 참여, 없으면 transaction 없이 실행
    • MANDATORY : 진행중인 transaction이 있으면 참여, 없으면 예외.
    • REQUIRES_NEW : 새로운 transaction 시작. 진행중인 transaction은 보류.
    • NOT_SUPPORTED : 진행중인 transaction이 있으면 보류, transaction 없이 실행.
    • NEVER : transaction없이 실행. 진행중인 transaction이 있으면 예외.
    • NESTED : 중첩 transaction 실행. 자식 tx은 부모 tx에게 영향을 주지 않지만, 부모는 자식에게 영향을 줌.

    application.yaml 설정

    3.PNG

    각각 DataSource Config 설정

    MainHikariSchemaDataSourceConfig 설정 - ‘TEST’ 스키마

    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
    54
    @Configuration
    @ConfigurationProperties(prefix = "spring.datasource." + "main")
    @EnableJpaRepositories(
    entityManagerFactoryRef = "mainEntityManagerFactory",
    transactionManagerRef = "mainTransactionManager",
    basePackages = {DatabaseConfig.BASE_ENTITY_PACKAGE_PREFIX + ".member"}
    )
    @MapperScan(
    basePackages = {DatabaseConfig.BASE_MAPPER_PACKAGE_PREFIX + ".member"}
    )
    public class MainHikariSchemaDataSourceConfig extends DatabaseConfig {
    final String name = "main";

    @Bean(name = name + "DataSource")
    @Primary
    public DataSource dataSource() {
    return new LazyConnectionDataSourceProxy(new HikariDataSource(this));
    }

    @Bean(name = name + "SessionFactory")
    @Primary
    public SqlSessionFactory sqlSessionFactory(@Qualifier(name + "DataSource") DataSource dataSource) throws Exception {
    SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
    setConfigureSqlSessionFactory(sessionFactoryBean, dataSource);
    return sessionFactoryBean.getObject();
    }

    @Bean(name = name + "SqlSessionTemplate")
    @Primary
    public SqlSessionTemplate firstSqlSessionTemplate(@Qualifier(name + "SessionFactory") SqlSessionFactory sqlSessionFactory) {
    return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean(name = name + "EntityManagerFactory")
    @Primary
    public EntityManagerFactory entityManagerFactory(@Qualifier(name + "DataSource") DataSource dataSource) {
    LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
    factory.setDataSource(dataSource);
    factory.setPackagesToScan("com.transaction.entities");
    factory.setPersistenceUnitName(name);
    setConfigureEntityManagerFactory(factory);


    return factory.getObject();
    }

    @Bean(name = name + "TransactionManager")
    @Primary
    public PlatformTransactionManager transactionManager(@Qualifier(name + "EntityManagerFactory") EntityManagerFactory entityManagerFactory) {
    JpaTransactionManager tm = new JpaTransactionManager();
    tm.setEntityManagerFactory(entityManagerFactory);
    return tm;
    }
    }

    SubHikariSchemaDataSourceConfig 설정 - ‘TEST2’ 스키마

    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
    @Configuration
    @ConfigurationProperties(prefix = "spring.datasource." + "sub")
    @EnableJpaRepositories(
    entityManagerFactoryRef = "subEntityManagerFactory",
    transactionManagerRef = "subTransactionManager",
    basePackages = {DatabaseConfig.BASE_ENTITY_PACKAGE_PREFIX + ".role"}
    )
    @MapperScan(
    basePackages = {DatabaseConfig.BASE_MAPPER_PACKAGE_PREFIX + ".role"}
    )
    public class SubHikariSchemaDataSourceConfig extends DatabaseConfig {
    final String name = "sub";

    @Bean(name = name + "DataSource")
    public DataSource dataSource() {
    return new LazyConnectionDataSourceProxy(new HikariDataSource(this));
    }

    @Bean(name = name + "SessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier(name + "DataSource") DataSource dataSource) throws Exception {
    SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
    sessionFactoryBean.setDataSource(dataSource);
    return sessionFactoryBean.getObject();
    }

    @Bean(name = name + "SqlSessionTemplate")
    public SqlSessionTemplate firstSqlSessionTemplate(@Qualifier(name + "SessionFactory") SqlSessionFactory sqlSessionFactory) {
    return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean(name = name + "EntityManagerFactory")
    public EntityManagerFactory entityManagerFactory(@Qualifier(name + "DataSource") DataSource dataSource) {
    LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
    factory.setDataSource(dataSource);
    factory.setPackagesToScan("com.transaction.entities");
    factory.setPersistenceUnitName(name);
    setConfigureEntityManagerFactory(factory);

    return factory.getObject();
    }

    @Bean(name = name + "TransactionManager")
    public PlatformTransactionManager transactionManager(@Qualifier(name + "EntityManagerFactory") EntityManagerFactory entityManagerFactory) {
    JpaTransactionManager tm = new JpaTransactionManager();
    tm.setEntityManagerFactory(entityManagerFactory);


    return tm;
    }
    }

    CASE 분별

    1. CASE (http://localhost:9200/required-required)
      • TransactionService (MainTransactionManager - Propagation.REQUIRED) -> MemberService (Mybatis - Insert) -> RoleService (Mybatis - Insert)
    member Propagation role Propagation member Insert role Insert
    REQUIRED REQUIRED rollback rollback

    성공적으로 원하는 의도대로 rollback 이 되었다. 아직 안심하기 이르다.

    1. CASE (http://localhost:9200/jpa-required-required)
      • TransactionService (MainTransactionManager - Propagation.REQUIRED) -> MemberService (JPA - Insert) -> RoleService (JPA - Insert)
    member Propagation role Propagation member Insert role Insert
    REQUIRED REQUIRED rollback commit

    JPA 경우 role 테이블은 rollback 이 안되었다.

    1. CASE (http://localhost:9200/required-required)
      • TransactionService (SubTransactionManager - Propagation.REQUIRED) -> MemberService (Mybatis - Insert) -> RoleService (Mybatis - Insert)
    member Propagation role Propagation member Insert role Insert
    REQUIRED REQUIRED commit commit

    이 경우 모두 rollback 이 안되었다.

    1. CASE (http://localhost:9200/jpa-required-required)
      • TransactionService (SubTransactionManager - Propagation.REQUIRED) -> MemberService (JPA - Insert) -> RoleService (JPA - Insert)
    member Propagation role Propagation member Insert role Insert
    REQUIRED REQUIRED commit rollback

    반대로 member 테이블에 rollback 이 안되었다.

    해결 방안

    이 경우 각각 설정한 TransactionManager 를 묶을 수 있는 ChainedTransactionManager 사용하기

    JtaTransactionManager 사용하는 방법이다.

    ChainedTransactionManager 사용하기

    @Configuration

    1
    2
    3
    4
    5
    6
    7
    8
    public class ChainedTxConfig {

    @Bean
    @Primary
    public PlatformTransactionManager transactionManager(PlatformTransactionManager mainTransactionManager, PlatformTransactionManager subTransactionManager) {
    return new ChainedTransactionManager(mainTransactionManager, subTransactionManager);
    }
    }

    각각 TransactionManager 를 설정 한것을 모두 가져와 ChainedTransactionManager 로 묶는다.

    여기서 주의 할 점은 ChainedTransactionManager Class 확인 해보면 getTransaction 메서드 및 commit rollback 메서드가 확인 할수 있다.

    getTransaction 메서드는 TransactionManager List 에 순차적으로 저장된 TransactionManager 부터 시작하는데 commit or rollback 은 거꾸로 실행한다.

    즉 트랜잭션 체인으로 묶을때 트랜잭션이 제일많이 발생하는 트랜잭션을 최우선순위 둬야한다.

    1. CASE (http://localhost:9200/required-required)
      • TransactionService (ChainedTransactionManager - Propagation.REQUIRED) -> MemberService (Mybatis - Insert) -> RoleService (Mybatis - Insert)
    member Propagation role Propagation member Insert role Insert
    REQUIRED REQUIRED rollback rollback
    1. CASE (http://localhost:9200/jpa-required-required)
      • TransactionService (ChainedTransactionManager - Propagation.REQUIRED) -> MemberService (JPA - Insert) -> RoleService (JPA - Insert)
    member Propagation role Propagation member Insert role Insert
    REQUIRED REQUIRED rollback rollback

    성공적이다.


    Copyright 2020- syh8088. 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.

    💰

    ×

    Help us with donation