ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring & JTA(분산 트랜잭션)
    Spring 2023. 10. 30. 15:58
    반응형

    Spring Boot에 JTA 사용하기

    안녕하세요. 기존 게시물 중 Spring Multi Datasource와 Multi Transaction 게시 글들이 있는데 말이 Multi Transaction이었지 결국에는 별개의 Transaction이었기 때문에 서로 다른 Transaction를 같이 사용했을 때 ACID가 지켜지지 않는 문제가 있었습니다.

    그래서 이번 게시 글에선 JTA(Java Transaction API)를 이용한 분산 트랜잭션 처리를 다뤄보고자 합니다. 여기서 JTA에 간략하게 설명하자면 분산 트랜잭션을 관리하는 데 사용되는 Java 표준 API이며 데이터베이스, 메시지 큐, 웹 서비스 등 분산 트래잭션을 처리하게 해줍니다. 즉 이기종 간의 트랜잭션을 처리할 수 있습니다.

    예로 A DB는 Postgresql이고 B DB는 Mysql인 경우 서로 다른 DB이기 때문에 당연히 트랜잭션이 다르게 됩니다. 이 상황에서 A, B에 같이 저장해야 하는 경우가 생긴다면 서로 다른 트랜잭션을 가지고 있으므로 에러가 발생한다면 한쪽에는 저장이 되고 한쪽에는 롤백이 되는 현상이 발생 수 있습니다. 이처럼 트랜잭션이 분산된 상황에서 JTA를 활용하면 서로 다른 DB라 하더라도 하나의 트랜잭션으로 저장과 롤백이 가능해집니다.

    JTA도 JPA처럼 개발자가 JPA 인터페이스들을 직접 구현하지 않고 Hibernate와 같은 라이브러리를 사용하듯이 구현되어 있는 라이브러리를 사용합니다.

    사용을 알아보기 앞서 JTA 사용 시 구현해야 하는 인터페이스들을 보자면

    • UserTransaction
    • TransactionManager
    • XAConnection
    • XAResource
    • XADataSource
    • Xid

    구현 클래스들이 필요합니다. 여기서 UserTransaction, TransactionManager 구현체는 JTA 라이브러리를 사용하지만 XA로 붙은 구현체들을 각 밴더사들이 클라이언트 드라이버에서 구현해서 제공합니다. 즉 JTA 사용 시 JTA 라이브러리만 필요로 하는게 아니라 XA가 구현 클라이언트 드라이버도 필요합니다.

    Spring과 연동 시에는 JTA 라이브러리의 UserTransaction, TransactionManager 구현체를 Bean으로 등록하고 Spring의 TransactinManager 중 JtaTransactionManager 구현체를 Bean으로 등록하기만 하면 됩니다.

    기술

    DB Postgresql-15-alpine 2개(Docker)

    Spring boot 2.7.16과 3.1.4

    JTA Atomikos

    Java Temurin 17

    Gradle 7.6.2

    예제의 DB 명칭은 Single A, Single B로 되어있습니다.

    예제

    예제는

    • Mybatis를 활용한 단일 트랜잭션
    • Mybatis를 활용한 JTA 트랜잭션
    • JPA를 활용한 단일 트랜잭션
    • JPA를 활용한 JTA 트랜잭션

    으로 구성 되어 있으며 예제 브렌치에 따라 Spring boot 2.7.16 구성과 3.1.4 구성으로 나눠져 있습니다.

    단일 트랜잭션과 JTA 트랜잭션을 혼합 구성하였는데 이유는

    • JTA를 서비스 초기부터 사용하기 보단 유지보수 중간에 도입하는 경우가 많다고 생각하여 기존 사용 중인 단일 트랜잭션를 그대로 유지하면서 JTA 트랜잭션을 추가로 사용할 수 있는 방안
    • Spring boot에서 일반적으로 HikariDataSoruce를 많이 사용하는데 JTA는 HikariDataSoruce가 아닌 별도 DataSource를 사용해야 하기 때문에 DataSoruce의 최적화를 고려해서 단일 트랜잭션만 사용해야 경우에는 HikariDataSoruce를 분산 트랜잭션인 경우에는 JTA DataSource를 분리해서 사용

    그리고 Spring boot를 2.7.16과 3.1.4 두 개로 구성한 이유는 Spring boot가 3.1.4로 업그레이드 하면서 Java EE가 아닌 Jakarta EE로 변경됨으로서 특정 API 패키지명이 변경되었습니다. 하필 변경사항 중에 JTA의 패키지도 javax.transaction가 아닌 jakarta.transaction으로 변경되었으며 JPA도 javax.persistence에서 jakarta.persistence로 변경되었기에 의존 라이브러리와 변경해야 할 사항들에 대해서 파악하고자 하여 2개로 구성하였습니다.

    Jakarta EE 변경에 대한 자세한 사항은 참고하시면 되겠습니다.

    구성

    Mybatis 구성은 아래 이미지를 참고하시면 되겠습니다.

    JTA-Mybatis.png

    DataSoruce 영역은 HikariDataSoruce와 JTA를 위한 Atomikos DataSource 2개로 분류 되며 단일 트랜잭션인 경우에는 HikariDataSoruce를 JTA인 경우에는 Atomikos DataSource를 사용하게 됩니다. 이때 Atomikos DataSource의 클라이언트 드라이버는 XA가 지원되는 클라이언트 드라이버여야 합니다.

    TransactionManager 영역은 단일 트랜잭션인 경우 HikariDataSoruce를 참조하는 TransactionManager를 가지며 JTA 트랜잭션은 각 DB마다 설정한 Atomikos DataSource를 참조하는 TransactionManager를 가지게 됩니다.

    MapperScan 영역인 경우 제일 중요한 부분인데 단일 트랜잭션의 패키지 경로와 JTA 트랜잭션 패키지 경로를 함께 사용하면 안됩니다. 패지키 경로는 둘 다 달라야 하기 때문에 JTA용 패키지 경로를 생성한 다음 해당 Mapper 인터페이스들은 단일 트랜잭션의 패키지 경로에 있는 Mapper 인터페이스들을 상속 받게 됩니다. 이와 같이 하게 되면 코드의 중복을 최소화할 수 있습니다.

    JPA 구성은 아래 이미지를 참고하시면 되겠습니다.

    JTA-JPA.png

    DataSource 영역은 위 Mybatis의 DataSource 영역과 동일합니다.

    TransactionManager 영역도 위 Mybatis의 TransactionManager 영역과 동일합니다.

    Repository 영역도 위 Mybatis의 MapperScan과 동일하게 같은 패키지 경로를 사용하면 안되기 때문에 JTA용 패키지 경로를 생성하고 단일 트랜잭션 패키지 경로를 부모 인터페이스로 새로운 패키지 경로를 생성합니다.

    Domain 영역은 단일 트랜잭션과 JTA 트랜잭션이 함께 참조해도 무관합니다.

    코드

    코드들을 살펴보기 앞서 예제 코드에서 나오는 사용자 편의 어노테이션을 먼저 설명하도록 하겠습니다.

    사용해야 할 트랜잭션이 여러 개 생겼기 때문에 @Transactional 어노테이션을 사용할 경우 매번 transactionManager 속성으로 어떤 트랜잭션을 사용할지 지정해줘야 합니다. 여기서 문제는 값이 문자열이기 때문에 오타가 나거나 잘못 지정 했을 경우 빠르게 알기 어렵다는 문제가 있습니다. 그래서 이를 문자열로 관리하는게 아닌 어노테이션을 관리하는 예제로 되어 있습니다.

    /**
     * <pre>
     *     Single-A의 JPA {@link Transactional}.
     *
     *     Spring {@link Transactional}를 활용한 커스텀 어노테이션.
     * </pre>
     */
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    @Transactional(transactionManager = "singleAJpaTransactionManager")
    public @interface SingleAJpaTransactional {
        /**
         * Alias for {@link Transactional#propagation}.
         */
        @AliasFor(annotation = Transactional.class)
        Propagation propagation() default Propagation.REQUIRED;
    
        /**
         * Alias for {@link Transactional#isolation}.
         */
        @AliasFor(annotation = Transactional.class)
        Isolation isolation() default Isolation.DEFAULT;
    
        /**
         * Alias for {@link Transactional#timeout}.
         */
        @AliasFor(annotation = Transactional.class)
        int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
    
        /**
         * Alias for {@link Transactional#readOnly}.
         */
        @AliasFor(annotation = Transactional.class)
        boolean readOnly() default false;
    
        /**
         * Alias for {@link Transactional#rollbackFor}.
         */
        @AliasFor(annotation = Transactional.class)
        Class<? extends Throwable>[] rollbackFor() default {};
    
        /**
         * Alias for {@link Transactional#rollbackForClassName}.
         */
        @AliasFor(annotation = Transactional.class)
        String[] rollbackForClassName() default {};
    
        /**
         * Alias for {@link Transactional#noRollbackFor}.
         */
        @AliasFor(annotation = Transactional.class)
        Class<? extends Throwable>[] noRollbackFor() default {};
    
        /**
         * Alias for {@link Transactional#noRollbackForClassName}.
         */
        @AliasFor(annotation = Transactional.class)
        String[] noRollbackForClassName() default {};
    }

    @Transactional(transactionManager = "singleAJpaTransactionManager"와 같이 사용해야 할 TransactionManager가 선언되어 있는 어노테이션을 생성하였습니다. 해당 어노테이션은 각 트랜백션 별로 생성되어 있습니다. 한 가지 단점은 Spring의 @Transactional 를 모태로 구성되어 있기 때문에 Spring에서 @Transactional의 변경이 생길 경우 해당 인터페이스들도 동일하게 수정 해줘야 합니다.

    각 트랜잭션별 어노테이션은 아래를 참고하시면 되겠습니다.

    @JtaTransactional JTA 트랜잭션

    @SingleAJpaTransactional JPA Single A 트랜잭션

    @SingleBJpaTransactional JPA Single B 트랜잭션

    @SingleATransactional Mybatis Sinalge A 트랜잭션

    @SingleBTransactional Mybatis Sinalge B 트랜잭션

    DataSource

    단일 트랜잭션

    단일 트랜잭션의 DataSource는 일반적인 구성과 동일합니다.

    • Single A DataSource
    package com.datasource.spring.config.db.postgresql.datasource;
    
    import com.zaxxer.hikari.HikariDataSource;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    
    /**
     * <pre>
     *     Single-A DB 데이터 소스 설정 클래스.
     * </pre>
     */
    @Configuration
    public class SingleADatasource {
        private final String driverClassName;
        private final String jdbcUrl;
        private final String username;
        private final String password;
    
        public SingleADatasource(@Value("${spring.datasource.postgres.singleA.driver-class-name}") String driverClassName
                , @Value("${spring.datasource.postgres.singleA.jdbc-url}") String jdbcUrl
                , @Value("${spring.datasource.postgres.singleA.username}") String username
                , @Value("${spring.datasource.postgres.singleA.password}") String password) {
            this.driverClassName = driverClassName;
            this.jdbcUrl = jdbcUrl;
            this.username = username;
            this.password = password;
        }
    
        /**
         * <pre>
         *     단일트랜잭션 데이터소스는 {@link HikariDataSource} 구현체를 사용.
         * </pre>
         */
        @Bean
        public DataSource postgresqlSingleADataSource() {
            HikariDataSource hikariDataSource = new HikariDataSource();
            hikariDataSource.setPoolName("pg-single-a");
            hikariDataSource.setDriverClassName(this.driverClassName);
            hikariDataSource.setUsername(this.username);
            hikariDataSource.setJdbcUrl(this.jdbcUrl);
            hikariDataSource.setPassword(this.password);
    
            return hikariDataSource;
        }
    
    }
    • Single B DataSource
    package com.datasource.spring.config.db.postgresql.datasource;
    
    import com.zaxxer.hikari.HikariDataSource;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    
    /**
     * <pre>
     *     Single-B DB 데이터 소스 설정 클래스.
     * </pre>
     */
    @Configuration
    public class SingleBDatasource {
        private final String driverClassName;
        private final String jdbcUrl;
        private final String username;
        private final String password;
    
        public SingleBDatasource(@Value("${spring.datasource.postgres.singleB.driver-class-name}") String driverClassName
                , @Value("${spring.datasource.postgres.singleB.jdbc-url}") String jdbcUrl
                , @Value("${spring.datasource.postgres.singleB.username}") String username
                , @Value("${spring.datasource.postgres.singleB.password}") String password) {
            this.driverClassName = driverClassName;
            this.jdbcUrl = jdbcUrl;
            this.username = username;
            this.password = password;
        }
    
        /**
         * <pre>
         *     단일트랜잭션 데이터소스는 {@link HikariDataSource} 구현체를 사용.
         * </pre>
         */
        @Bean
        public DataSource postgresqlSingleBDataSource() {
            HikariDataSource hikariDataSource = new HikariDataSource();
            hikariDataSource.setPoolName("pg-single-b");
            hikariDataSource.setDriverClassName(this.driverClassName);
            hikariDataSource.setUsername(this.username);
            hikariDataSource.setJdbcUrl(this.jdbcUrl);
            hikariDataSource.setPassword(this.password);
    
            return hikariDataSource;
        }
    
    }

    JTA 트랜잭션

    JTA인 경우엔 HikariDataSource가 아닌 Atomikos에서 제공하는 DataSource와 각 밴더사에서 제공하는 XA 드라이버 정보를 사용합니다.

    • Single A DataSource
    package com.datasource.spring.config.db.postgresql.datasource;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    import javax.sql.XADataSource;
    import java.util.Properties;
    
    /**
     * <pre>
     *     Single-A DB JTA 데이터 소스 설정 클래스.
     * </pre>
     */
    @Configuration
    public class JtaSingleADatasource {
        private final String driverClassName;
        private final String jdbcUrl;
        private final String username;
        private final String password;
    
        /**
         * <pre>
         *     단일트랜잭션의 접속정보와 동일하나 접속 시 필요한 드라이버는 {@link XADataSource}의 구현체여야 한다.
         * </pre>
         */
        public JtaSingleADatasource(@Value("${spring.datasource.jta.singleA.driver-class-name}") String driverClassName
                , @Value("${spring.datasource.jta.singleA.jdbc-url}") String jdbcUrl
                , @Value("${spring.datasource.jta.singleA.username}") String username
                , @Value("${spring.datasource.jta.singleA.password}") String password) {
            this.driverClassName = driverClassName;
            this.jdbcUrl = jdbcUrl;
            this.username = username;
            this.password = password;
        }
    
        /**
         * <pre>
         *     Postgresql {@link XADataSource} 구현체가 설정된 atomikos datasource 구현체 반환.
         * </pre>
         */
        @Bean(initMethod = "init", destroyMethod = "close")
        public DataSource jtaSingleADataSource() {
            AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
            atomikosDataSourceBean.setXaDataSourceClassName(driverClassName);
    
            Properties p = new Properties();
            p.setProperty("user", username);
            p.setProperty("password", password);
            p.setProperty("url", jdbcUrl);
    
            atomikosDataSourceBean.setXaProperties (p);
    
            return atomikosDataSourceBean;
        }
    
    }
    • Single B DataSource
    package com.datasource.spring.config.db.postgresql.datasource;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    import javax.sql.XADataSource;
    import java.util.Properties;
    
    /**
     * <pre>
     *     Single-B DB JTA 데이터 소스 설정 클래스.
     * </pre>
     */
    @Configuration
    public class JtaSingleBDatasource {
        private final String driverClassName;
        private final String jdbcUrl;
        private final String username;
        private final String password;
    
        /**
         * <pre>
         *     단일트랜잭션의 접속정보와 동일하나 접속 시 필요한 드라이버는 {@link XADataSource}의 구현체여야 한다.
         * </pre>
         */
        public JtaSingleBDatasource(@Value("${spring.datasource.jta.singleB.driver-class-name}") String driverClassName
                , @Value("${spring.datasource.jta.singleB.jdbc-url}") String jdbcUrl
                , @Value("${spring.datasource.jta.singleB.username}") String username
                , @Value("${spring.datasource.jta.singleB.password}") String password) {
            this.driverClassName = driverClassName;
            this.jdbcUrl = jdbcUrl;
            this.username = username;
            this.password = password;
        }
    
        /**
         * <pre>
         *     Postgresql {@link XADataSource} 구현체가 설정된 atomikos datasource 구현체 반환.
         * </pre>
         */
        @Bean(initMethod = "init", destroyMethod = "close")
        public DataSource jtaSingleBDataSource() {
            AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
            atomikosDataSourceBean.setXaDataSourceClassName(driverClassName);
    
            Properties p = new Properties();
            p.setProperty("user", username);
            p.setProperty("password", password);
            p.setProperty("url", jdbcUrl);
    
            atomikosDataSourceBean.setXaProperties (p);
    
            return atomikosDataSourceBean;
        }
    
    }

    위 설명에선 Atomikos에서 제공하는 DataSource를 사용한다고 했는데 import를 보면 Atomikos가 아닌 import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; 와 같이 Spring의 AtomikosDataSourceBean임을 알 수 있습니다.

    그러나 아래 해당 클래스 코드를 보면 Atomikos의 AtomikosDataSourceBean 를 상속하였기에 결국에는 Atomikos의 AtomikosDataSourceBean를 사용한다고 보면 됩니다. 그리고 혹시나 Spring의 AtomikosDataSourceBean가 아닌 Atomikos의 AtomikosDataSourceBean를 import 했다면 @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource") 설정이 없기 때문에 application.yml(xml)에서 설정한 값들이 정상 적용이 안될 수도 있습니다.

    @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource")
    public class AtomikosDataSourceBean extends com.atomikos.jdbc.AtomikosDataSourceBean
            implements BeanNameAware, InitializingBean, DisposableBean {
    
        private String beanName;
    
        @Override
        public void setBeanName(String name) {
            this.beanName = name;
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
            if (!StringUtils.hasLength(getUniqueResourceName())) {
                setUniqueResourceName(this.beanName);
            }
            init();
        }
    
        @Override
        public void destroy() throws Exception {
            close();
        }
    
    }

    Mybatis

    단일 트랜잭션

    DataSource가 필요한 Bean인 경우에는 각 접속해야 할 DataSource Bean을 의존해서 사용합니다. 그리고 해당 Config에서는 단일 트랜잭션 활용을 위해 각 Single A, B의 DataSourceTransactionManager 구현체를 Bean에 등록 합니다. 단일 트랜잭션인 경우에도 일반적인 Mybatis 구성과 크게 다른건 없습니다.

    • Single A Mybatis
    package com.datasource.spring.config.db.postgresql.mybatis.singleA;
    
    import com.datasource.spring.config.db.postgresql.datasource.SingleADatasource;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.SqlSessionTemplate;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import javax.sql.DataSource;
    
    /**
     * <pre>
     *     JTA가 아닌 단일트랜잭션 활용을 위한 Mybatis 설정
     *
     *     주의점
     *     - {@link MapperScan}의 basePackages 속성외 패키지 경로를 지정해야 하는 경우 JTA 트랜잭션을 활용하는 basePackages와 동일하면 안된다.
     * </pre>
     */
    @Configuration
    @EnableTransactionManagement
    @MapperScan(basePackages = {"com.datasource.repo.mybatis.single.singleA"}, sqlSessionFactoryRef = "postgresqlSingleASessionFactory")
    public class PostgresqlSingleAConfig {
    
        /**
         * <pre>
         *     {@link SqlSessionFactory} 구현체에서 사용되는 datasource는  {@link SingleADatasource#postgresqlSingleADataSource()}를 사용.
         * </pre>
         */
        @Bean
        public SqlSessionFactory postgresqlSingleASessionFactory(DataSource postgresqlSingleADataSource) throws Exception {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(postgresqlSingleADataSource);
            sqlSessionFactoryBean.setTypeAliasesPackage("com.datasource.repo.mybatis.single.singleA");
    
            sqlSessionFactoryBean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
    
            return sqlSessionFactoryBean.getObject();
        }
    
        @Bean
        public SqlSessionTemplate postgresSingleASessionTemplate(SqlSessionFactory postgresqlSingleASessionFactory) {
            return new SqlSessionTemplate(postgresqlSingleASessionFactory);
        }
    
        /**
         * <pre>
         *     Single-A를 활용한 Mubatis 단일트랜잭션 반환.
         *
         *     사용되는 datasource는  {@link SingleADatasource#postgresqlSingleADataSource()}를 사용.
         * </pre>
         */
        @Bean
        public PlatformTransactionManager singleATransactionManager(DataSource postgresqlSingleADataSource) {
            DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
            transactionManager.setDataSource(postgresqlSingleADataSource);
    
            return transactionManager;
        }
    }
    • Single B Mybatis
    package com.datasource.spring.config.db.postgresql.mybatis.singleB;
    
    import com.datasource.spring.config.db.postgresql.datasource.SingleBDatasource;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.SqlSessionTemplate;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import javax.sql.DataSource;
    
    /**
     * <pre>
     *     JTA가 아닌 단일트랜잭션 활용을 위한 Mybatis 설정
     *
     *     주의점
     *     - {@link MapperScan}의 basePackages 속성외 패키지 경로를 지정해야 하는 경우 JTA 트랜잭션을 활용하는 basePackages와 동일하면 안된다.
     * </pre>
     */
    @Configuration
    @EnableTransactionManagement
    @MapperScan(basePackages = {"com.datasource.repo.mybatis.single.singleB"}, sqlSessionFactoryRef = "postgresqlSingleBSessionFactory")
    public class PostgresqlSingleBConfig {
    
        /**
         * <pre>
         *     {@link SqlSessionFactory} 구현체에서 사용되는 datasource는  {@link SingleBDatasource#postgresqlSingleBDataSource()}를 사용.
         * </pre>
         */
        @Bean
        public SqlSessionFactory postgresqlSingleBSessionFactory(DataSource postgresqlSingleBDataSource) throws Exception {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(postgresqlSingleBDataSource);
            sqlSessionFactoryBean.setTypeAliasesPackage("com.datasource.repo.mybatis.single.singleB");
    
            sqlSessionFactoryBean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
    
            return sqlSessionFactoryBean.getObject();
        }
    
        @Bean
        public SqlSessionTemplate postgresSingleBSessionTemplate(SqlSessionFactory postgresqlSingleBSessionFactory) {
            return new SqlSessionTemplate(postgresqlSingleBSessionFactory);
        }
    
        /**
         * <pre>
         *     Single-B를 활용한 Mubatis 단일트랜잭션 반환.
         *
         *     사용되는 datasource는  {@link SingleBDatasource#postgresqlSingleBDataSource()}를 사용.
         * </pre>
         */
        @Bean
        public PlatformTransactionManager singleBTransactionManager(DataSource postgresqlSingleBDataSource) {
            DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
            transactionManager.setDataSource(postgresqlSingleBDataSource);
    
            return transactionManager;
        }
    }

    JTA 트랜잭션

    Mybatis JTA 트랜잭션인 경우에는 단일 트랜잭션과 다르게 트랜잭션을 JTA 하나로 사용하기에 별도의 트랜잭션 설정은 필요 없으며 @EnableTransactionManagement 어노테이션도 필요하지 않습니다. 단 SqlSessionFactoryBean 에 필요한 DataSource는 XA 드라이버를 사용한 DataSoruce를 사용하고 앞서 설명 했듯이 @MapperScanbasePackages속성의 패키지 경로를 단일 트랜잭션의 패키지 경로와 다르게 설정해야 합니다.

    • Single A JTA Mybatis
    package com.datasource.spring.config.db.postgresql.mybatis.singleA;
    
    import com.datasource.spring.config.db.postgresql.datasource.JtaSingleBDatasource;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    
    /**
     * <pre>
     *     JTA 활용을 위한 Mybatis 설정
     *
     *     주의점
     *     - {@link MapperScan}의 basePackages 속성외 패키지 경로를 지정해야 하는 경우 단일트랜잭션을 활용하는 basePackages와 동일하면 안된다.
     * </pre>
     */
    @Configuration
    @MapperScan(basePackages = {"com.datasource.repo.mybatis.jta.singleA"}, sqlSessionFactoryRef = "jtaSingleASqlSessionFactory")
    public class JtaSingleAConfig {
    
        /**
         * <pre>
         *     {@link SqlSessionFactory} 구현체에서 사용되는 datasource는  {@link JtaSingleBDatasource#jtaSingleBDataSource()}를 사용.
         * </pre>
         */
        @Bean
        public SqlSessionFactory jtaSingleASqlSessionFactory(DataSource jtaSingleADataSource) throws Exception {
            final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(jtaSingleADataSource);
            sqlSessionFactoryBean.setTypeAliasesPackage("com.datasource.repo.mybatis.jta.singleA");
    
            return sqlSessionFactoryBean.getObject();
        }
    }
    • Single B JTA Mybatis
    package com.datasource.spring.config.db.postgresql.mybatis.singleB;
    
    import com.datasource.spring.config.db.postgresql.datasource.JtaSingleBDatasource;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import javax.sql.DataSource;
    import java.util.Properties;
    
    /**
     * <pre>
     *     JTA 활용을 위한 Mybatis 설정
     *
     *     주의점
     *     - {@link MapperScan}의 basePackages 속성외 패키지 경로를 지정해야 하는 경우 단일트랜잭션을 활용하는 basePackages와 동일하면 안된다.
     * </pre>
     */
    @Configuration
    @MapperScan(basePackages = {"com.datasource.repo.mybatis.jta.singleB"}, sqlSessionFactoryRef = "jtaSingleBSqlSessionFactory")
    public class JtaSingleBConfig {
    
        /**
         * <pre>
         *     {@link SqlSessionFactory} 구현체에서 사용되는 datasource는  {@link JtaSingleBDatasource#jtaSingleBDataSource()}를 사용.
         * </pre>
         */
        @Bean
        public SqlSessionFactory jtaSingleBSqlSessionFactory(DataSource jtaSingleBDataSource) throws Exception {
            final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(jtaSingleBDataSource);
            sqlSessionFactoryBean.setTypeAliasesPackage("com.datasource.repo.mybatis.jta.singleB");
    
            return sqlSessionFactoryBean.getObject();
        }
    }

    JPA

    단일 트랜잭션

    JPA 단일 트랜잭션 설정도 기존 구성과 크게 다른 점은 없습니다. 다만 JPA 트랜잭션을 설정하는 singleAJpaTransactionManager, singleBJpaTransactionManager 메서드를 보면 @Qualifier 어노테이션으로 LocalContainerEntityManagerFactoryBean 객체를 주입 받는데 @Qualifier 어노테이션이 필요한 이유는 LocalContainerEntityManagerFactoryBean 객체가 인터페이스가 아닌 구현체이기 때문입니다. 해당 객체의 클래스는 한 개 이상의 인터페이스를 상속 받아 구현하였기에 특정 인터페이스의 타입으로 주입 받기 어렵기 때문에 @Qualifier 어노테이션으로 사용해 구현체로 주입 받습니다.

    • Single A JPA
    package com.datasource.spring.config.db.postgresql.jpa.singleA;
    
    import com.datasource.spring.config.db.postgresql.datasource.SingleADatasource;
    import org.springframework.beans.factory.annotation.Qualifier;
    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.JpaVendorAdapter;
    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;
    import java.util.Properties;
    
    /**
     * <pre>
     *     JTA가 아닌 단일트랜잭션 활용을 위한 JPA 설정
     *
     *     주의점
     *     - {@link EnableJpaRepositories}의 basePackages 속성이 JTA를 활용하는 basePackages와 동일하면 안된다.
     * </pre>
     */
    @Configuration
    @EnableTransactionManagement
    @EnableJpaRepositories(basePackages = "com.datasource.repo.jpa.single.singleA"
            , entityManagerFactoryRef = "singleAEntityManagerFactory"
            , transactionManagerRef = "singleAJpaTransactionManager")
    public class PostgresqlSingleAJpaConfig {
    
        /**
         * <pre>
         *     {@link LocalContainerEntityManagerFactoryBean} 구현체에서 사용되는 datasource는 {@link SingleADatasource#postgresqlSingleADataSource()}를 사용.
         * </pre>
         */
        @Bean
        public LocalContainerEntityManagerFactoryBean singleAEntityManagerFactory(DataSource postgresqlSingleADataSource) {
            LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
            em.setDataSource(postgresqlSingleADataSource);
            em.setPackagesToScan("com.datasource.domain.singleA");
    
            Properties properties = new Properties();
            properties.setProperty("hibernate.hbm2ddl.auto", "create-drop");
            properties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQL95Dialect");
            properties.setProperty("hibernate.show_sql", "true");
            properties.setProperty("hibernate.format_sql", "true");
    
            JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
            em.setJpaVendorAdapter(vendorAdapter);
            em.setJpaProperties(properties);
    
            return em;
        }
    
        /**
         * <pre>
         *     Single-A를 활용한 JPA 단일트랜잭션 반환.
         * </pre>
         */
        @Bean
        public PlatformTransactionManager singleAJpaTransactionManager(@Qualifier("singleAEntityManagerFactory") LocalContainerEntityManagerFactoryBean singleAEntityManagerFactory) {
            JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
            jpaTransactionManager.setEntityManagerFactory(singleAEntityManagerFactory.getObject());
    
            return jpaTransactionManager;
        }
    }
    • Single B JPA
    package com.datasource.spring.config.db.postgresql.jpa.singleB;
    
    import com.datasource.spring.config.db.postgresql.datasource.SingleBDatasource;
    import org.springframework.beans.factory.annotation.Qualifier;
    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.JpaVendorAdapter;
    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;
    import java.util.Properties;
    
    /**
     * <pre>
     *     JTA가 아닌 단일트랜잭션 활용을 위한 JPA 설정
     *
     *     주의점
     *     - {@link EnableJpaRepositories}의 basePackages 속성이 JTA를 활용하는 basePackages와 동일하면 안된다.
     * </pre>
     */
    @Configuration
    @EnableTransactionManagement
    @EnableJpaRepositories(basePackages = "com.datasource.repo.jpa.single.singleB"
            , entityManagerFactoryRef = "singleBEntityManagerFactory"
            , transactionManagerRef = "singleBJpaTransactionManager")
    public class PostgresqlSingleBJtaJpaConfig {
    
        /**
         * <pre>
         *     {@link LocalContainerEntityManagerFactoryBean} 구현체에서 사용되는 datasource는 {@link SingleBDatasource#postgresqlSingleBDataSource()}를 사용.
         * </pre>
         */
        @Bean
        public LocalContainerEntityManagerFactoryBean singleBEntityManagerFactory(DataSource postgresqlSingleBDataSource) {
            LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
            em.setDataSource(postgresqlSingleBDataSource);
            em.setPackagesToScan("com.datasource.domain.singleB");
    
            Properties properties = new Properties();
            properties.setProperty("hibernate.hbm2ddl.auto", "create-drop");
            properties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQL95Dialect");
            properties.setProperty("hibernate.show_sql", "true");
            properties.setProperty("hibernate.format_sql", "true");
    
            JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
            em.setJpaVendorAdapter(vendorAdapter);
            em.setJpaProperties(properties);
    
            return em;
        }
    
        /**
         * <pre>
         *     Single-B를 활용한 JPA 단일트랜잭션 반환.
         * </pre>
         */
        @Bean
        public PlatformTransactionManager singleBJpaTransactionManager(@Qualifier("singleBEntityManagerFactory") LocalContainerEntityManagerFactoryBean singleBEntityManagerFactory) {
            JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
            jpaTransactionManager.setEntityManagerFactory(singleBEntityManagerFactory.getObject());
    
            return jpaTransactionManager;
        }
    }

    JTA 트랜잭션

    JTA를 이용한 JPA 트랜잭션도 위 Mybatis와 마찬가지로 JTA 하나로 트랜잭션을 사용하기에 별도 트랜잭션 설정은 필요 없고 @EnableTransactionManagement 어노테이션도 필요하지 않습니다. 단 LocalContainerEntityManagerFactoryBean 구현체에서 사용하는 DataSource는 XA 드라이버를 사용한 DataSource여야 합니다.(확인 했을 땐 꼭 XA 드라이버 DataSource가 아니여도 정상 동작은 하였으나 일관성을 위해 XA 드라이버인 DataSource를 사용하였습니다.)

    @EnableJpaRepositories 속성 중 basePackages 도 단일 트랜잭션의 패키지 경로와 동일하면 안되기에 JTA용 패키지 경로를 생성해서 설정합니다. 그러나 LocalContainerEntityManagerFactoryBean#setPackageToScan의 도메인 패키지 경로는 단일 트랜잭션과 동일한 도메인 패키지 경로를 사용합니다.

    transactionManagerRef 속성은 Single A, B 둘 다 동일하게 JTA 트랜잭션으로 설정합니다.

    그리고 JPA에 JTA 설정을 아래와 같이 합니다. 해당 예제는 Atomikos를 사용하기에 AtomikosJtaPlatform 로 설정하였습니다. Hibernate에선 해당 구현체 외 다른 JTA 구현체들도 제공하고 있습니다. 그러나 별도로 구현해야 한다면 Hibernate의 AbstractJtaPlatform를 상속 받아 locateTransactionManager, locateUserTransaction 메서드를 구현하셔야 합니다.

            properties.setProperty("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName());
            properties.setProperty("javax.persistence.transactionType", "JTA");
    • Single A JTA JPA
    package com.datasource.spring.config.db.postgresql.jpa.singleA;
    
    import com.datasource.spring.config.db.postgresql.datasource.JtaSingleADatasource;
    import org.hibernate.engine.transaction.jta.platform.internal.AtomikosJtaPlatform;
    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.JpaVendorAdapter;
    import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
    import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
    
    import javax.sql.DataSource;
    import java.util.Properties;
    
    /**
     * <pre>
     *     JTA 활용을 위한 JPA 설정
     *
     *     주의점
     *     - {@link EnableJpaRepositories}의 basePackages 속성이 단일트랜잭션을 활용하는 basePackages와 동일하면 안된다.
     *     - {@link LocalContainerEntityManagerFactoryBean#setPackagesToScan(String...)}의 패키지는 동일해야 한다.
     * </pre>
     */
    @Configuration
    @EnableJpaRepositories(basePackages = "com.datasource.repo.jpa.jta.singleA"
            , entityManagerFactoryRef = "singleAJtaEntityManagerFactory"
            , transactionManagerRef = "jtaSingleTransactionManager")
    public class JtaJpaSingleAConfig {
    
        /**
         * <pre>
         *     {@link LocalContainerEntityManagerFactoryBean} 구현체에서 사용되는 datasource는 {@link JtaSingleADatasource#jtaSingleADataSource()}를 사용.
         * </pre>
         */
        @Bean
        public LocalContainerEntityManagerFactoryBean singleAJtaEntityManagerFactory(DataSource jtaSingleADataSource) {
            LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
            em.setDataSource(jtaSingleADataSource);
            em.setPackagesToScan("com.datasource.domain.singleA");
    
            Properties properties = new Properties();
            properties.setProperty("hibernate.hbm2ddl.auto", "create-drop");
            properties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQL95Dialect");
    
            // JPA에서 JTA 활용을 위한 설정.
            properties.setProperty("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName());
            properties.setProperty("javax.persistence.transactionType", "JTA");
    
            JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
            em.setJpaVendorAdapter(vendorAdapter);
            em.setJpaProperties(properties);
    
            return em;
        }
    }
    • Single B JTA JPA
    package com.datasource.spring.config.db.postgresql.jpa.singleB;
    
    import com.datasource.spring.config.db.postgresql.datasource.JtaSingleBDatasource;
    import org.hibernate.engine.transaction.jta.platform.internal.AtomikosJtaPlatform;
    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.JpaVendorAdapter;
    import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
    import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
    
    import javax.sql.DataSource;
    import java.util.Properties;
    
    /**
     * <pre>
     *     JTA 활용을 위한 JPA 설정
     *
     *     주의점
     *     - {@link EnableJpaRepositories}의 basePackages 속성이 단일트랜잭션을 활용하는 basePackages와 동일하면 안된다.
     *     - {@link LocalContainerEntityManagerFactoryBean#setPackagesToScan(String...)}의 패키지는 동일해야 한다.
     * </pre>
     */
    @Configuration
    @EnableJpaRepositories(basePackages = "com.datasource.repo.jpa.jta.singleB"
            , entityManagerFactoryRef = "singleBJtaEntityManagerFactory"
            , transactionManagerRef = "jtaSingleTransactionManager")
    public class JtaJpaSingleBConfig {
    
        /**
         * <pre>
         *     {@link LocalContainerEntityManagerFactoryBean} 구현체에서 사용되는 datasource는 {@link JtaSingleBDatasource#jtaSingleBDataSource()}를 사용.
         * </pre>
         */
        @Bean
        public LocalContainerEntityManagerFactoryBean singleBJtaEntityManagerFactory(DataSource jtaSingleBDataSource) {
            LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
            em.setDataSource(jtaSingleBDataSource);
            em.setPackagesToScan("com.datasource.domain.singleB");
    
            Properties properties = new Properties();
            properties.setProperty("hibernate.hbm2ddl.auto", "create-drop");
            properties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQL95Dialect");
    
            // JPA에서 JTA 활용을 위한 설정.
            properties.setProperty("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName());
            properties.setProperty("javax.persistence.transactionType", "JTA");
    
            JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
            em.setJpaVendorAdapter(vendorAdapter);
            em.setJpaProperties(properties);
    
            return em;
        }
    }

    JTA

    JTA 트랜잭션 설정은 간단합니다. 위에서 설명 했듯이 UserTransactionUserTransactionManager 인터페이스의 구현체들을 Bean에 등록만 하면 됩니다. 마찬가지로 예제는 Atomikos를 사용하기에 Atomikos의 구현체를 Bean에 등록합니다.

    그리고 PlatformTransactionManager 구현체들 중 JtaTransactionManager 구현체를 Bean에 등록해 JTA 트랜잭션을 사용합니다.

    package com.datasource.spring.config.db.jta;
    
    import com.atomikos.icatch.jta.UserTransactionImp;
    import com.atomikos.icatch.jta.UserTransactionManager;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    import org.springframework.transaction.jta.JtaTransactionManager;
    
    import javax.transaction.TransactionManager;
    import javax.transaction.UserTransaction;
    
    /**
     * <pre>
     *     JTA 트랜잭션 설정 클래스.
     *
     *     자바의 JTA 사용을 위해 주요 인터페이스인
     *     {@link UserTransaction}
     *     {@link UserTransactionManager}
     *     구현체를 반환한다.
     *     구현체는 atomikos를 활용.
     * </pre>
     */
    @Configuration
    @EnableTransactionManagement
    public class JtaSingleConfig {
    
        /**
         * <pre>
         *     atomikos의 {@link UserTransactionImp} 반환
         * </pre>
         */
        @Bean
        public UserTransaction userTransaction() throws Throwable {
            UserTransactionImp userTransactionImp = new UserTransactionImp();
            userTransactionImp.setTransactionTimeout(10000);
    
            return userTransactionImp;
        }
    
        /**
         * <pre>
         *     atomikos의 {@link UserTransactionManager} 반환
         * </pre>
         */
        @Bean(initMethod = "init", destroyMethod = "close")
        public TransactionManager atomikosTransactionManager() {
            UserTransactionManager userTransactionManager = new UserTransactionManager();
            userTransactionManager.setForceShutdown(false);
    
            return userTransactionManager;
        }
    
        /**
         * <pre>
         *     Spring에서 트랜잭션 활용을 위해  {@link PlatformTransactionManager} 구현체 중
         *     {@link JtaTransactionManager} 구현체 반환.
         * </pre>
         */
        @Bean
        public PlatformTransactionManager jtaSingleTransactionManager(UserTransaction userTransaction
                , TransactionManager atomikosTransactionManager) {
            JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(userTransaction, atomikosTransactionManager);
    
            return jtaTransactionManager;
        }
    }

    Mapper & Repository

    Mapper(단일 트랜잭션)

    • SingleAMapper

    Mybatis Single A DB용 Mapper 인터페이스 입니다.

    package com.datasource.repo.mybatis.single.singleA;
    
    import com.datasource.repo.mybatis.jta.singleA.JtaSingleAMapper;
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Select;
    
    /**
     * <pre>
     *     기존 SingleAMapper
     *
     *     해당 인터페이스는 JTA 활용을 위해 {@link JtaSingleAMapper}의 부모인터페이스가 된다.
     * </pre>
     */
    @Mapper
    public interface SingleAMapper {
        @Insert("""
                insert into test(
                    test_text
                ) values (
                    #{testText}
                )
        """)
        long saveTest(String testText);
    
        @Select("""
                select 
                    test_text 
                from test 
                where test_text = #{testText}
        """)
        String findTestText(String testText);
    }
    • SingleBMapper

    Mybatis Single B DB용 Mapper 인터페이스 입니다.

    package com.datasource.repo.mybatis.single.singleB;
    
    import com.datasource.repo.mybatis.jta.singleB.JtaSingleBMapper;
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Select;
    
    /**
     * <pre>
     *     기존 SingleBMapper
     *
     *     해당 인터페이스는 JTA 활용을 위해 {@link JtaSingleBMapper}의 부모인터페이스가 된다.
     * </pre>
     */
    @Mapper
    public interface SingleBMapper {
        @Insert("""
                insert into test(
                    test_text
                ) values (
                    #{testText}
                )
        """)
        long saveTest(String testText);
    
        @Select("""
                select 
                    test_text 
                from test 
                where test_text = #{testText}
        """)
        String findTestText(String testText);
    
    }

    Mapper(JTA 트랜잭션)

    앞서 계속 설명 드렸지만 패지키 경로를 달리 하기 위해 JTA용 Mapper 인터페이스를 별도로 생성하였고 단일 트랜잭션 Mapper 인터페이스들을 상속 받습니다. 그러나 이왕 분리 되었기 때문에 단일 트랜잭션엔 필요없고 JTA만 활용한다면 해당 인터페이스에 선언함으로서 단일 트랜잭션과 JTA 트랜잭션의 결합도를 낯추고 단일 & JTA 전부 사용하는 경우면 단일 트랜잭션 Mapper 인터페이스에 선언함으로 응집도를 높일 수 있습니다.

    • JtaSingleAMapper
    package com.datasource.repo.mybatis.jta.singleA;
    
    import com.datasource.repo.mybatis.single.singleA.SingleAMapper;
    import org.apache.ibatis.annotations.Mapper;
    import org.mybatis.spring.annotation.MapperScan;
    
    /**
     * <pre>
     *     JTA SingleAMapper
     *
     *     기존 사용 중인 {@link MapperScan}의 basePackages 영역과 충돌 방지를 위해
     *     JTA용 {@link MapperScan} basePackages 영역을 가진다.
     *
     *     기존 Mapper 기능들을 그대로 사용 하기 위해 {@link SingleAMapper}를 상속 받는다.
     * </pre>
     */
    @Mapper
    public interface JtaSingleAMapper extends SingleAMapper {
    }
    • JtaSingleBMapper
    package com.datasource.repo.mybatis.jta.singleB;
    
    import com.datasource.repo.mybatis.single.singleB.SingleBMapper;
    import org.apache.ibatis.annotations.Mapper;
    import org.mybatis.spring.annotation.MapperScan;
    
    /**
     * <pre>
     *     JTA SingleBMapper
     *
     *     기존 사용 중인 {@link MapperScan}의 basePackages 영역과 충돌 방지를 위해
     *     JTA용 {@link MapperScan} basePackages 영역을 가진다.
     *
     *     기존 Mapper 기능들을 그대로 사용 하기 위해 {@link SingleBMapper}를 상속 받는다.
     * </pre>
     */
    @Mapper
    public interface JtaSingleBMapper extends SingleBMapper {
    }

    Repository(단일 트랜잭션)

    • SingleARepository

    JPA Single A DB용 Repository 인터페이스 입니다.

    package com.datasource.repo.jpa.single.singleA;
    
    import com.datasource.domain.singleA.SingleAJpa;
    import com.datasource.repo.jpa.jta.singleA.JtaSingleARepository;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;
    
    /**
     * <pre>
     *     기존 SingleARepository
     *
     *     해당 인터페이스는 JTA 활용을 위해 {@link JtaSingleARepository}의 부모인터페이스가 된다.
     * </pre>
     */
    @Repository
    public interface SingleARepository extends JpaRepository<SingleAJpa, Long> {
    }
    • SingleBRepository

    JPA Single B DB용 Repository 인터페이스 입니다.

    package com.datasource.repo.jpa.single.singleB;
    
    import com.datasource.domain.singleB.SingleBJpa;
    import com.datasource.repo.jpa.jta.singleB.JtaSingleBRepository;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;
    
    /**
     * <pre>
     *     기존 SingleBRepository
     *
     *     해당 인터페이스는 JTA 활용을 위해 {@link JtaSingleBRepository}의 부모인터페이스가 된다.
     * </pre>
     */
    @Repository
    public interface SingleBRepository extends JpaRepository<SingleBJpa, Long> {
    }

    Repository(JTA 트랜잭션)

    Mapper(JTA 트랜잭션) 설명과 동일합니다. 패키지 경로를 달리하기 위해 단일 트랜잭션의 Repository 인터페이스를 상속 받은 별도의 패키지 경로를 생성합니다. 마찬가지로 상황에 따라 결합도를 낮추고 응집도를 높일 수 있습니다.

    • JtaSingleARepository
    package com.datasource.repo.jpa.jta.singleA;
    
    import com.datasource.repo.jpa.single.singleA.SingleARepository;
    import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
    import org.springframework.stereotype.Repository;
    
    /**
     * <pre>
     *     JTA SingleARepository
     *
     *     기존 사용 중인 {@link EnableJpaRepositories}의 basePackages 영역과 충돌 방지를 위해
     *     JTA용 {@link EnableJpaRepositories} basePackages 영역을 가진다.
     *
     *     기존 Repository 기능들을 그대로 사용 하기 위해 {@link SingleARepository}를 상속 받는다.
     * </pre>
     */
    @Repository
    public interface JtaSingleARepository extends SingleARepository {
    }
    • JtaSingleBRepository
    package com.datasource.repo.jpa.jta.singleB;
    
    import com.datasource.repo.jpa.single.singleB.SingleBRepository;
    import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
    import org.springframework.stereotype.Repository;
    
    /**
     * <pre>
     *     JTA SingleBRepository
     *
     *     기존 사용 중인 {@link EnableJpaRepositories}의 basePackages 영역과 충돌 방지를 위해
     *     JTA용 {@link EnableJpaRepositories} basePackages 영역을 가진다.
     *
     *     기존 Repository 기능들을 그대로 사용 하기 위해 {@link SingleBRepository}를 상속 받는다.
     * </pre>
     */
    @Repository
    public interface JtaSingleBRepository extends SingleBRepository {
    }

    Domain

    JPA에서 사용하는 도메인 입니다.

    SingleAJpa

    Single A DB에서 사용하는 도메인입니다.

    package com.datasource.domain.singleA;
    
    import javax.persistence.*;
    
    /**
     * <pre>
     *     JPA용 JTA 테스트를 위한 엔티티
     *     해당 엔티티는 Singel A(Postgresql) DB에서 사용.
     * </pre>
     */
    @Entity
    @Table(name = "single_a_jpa")
    public class SingleAJpa {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long seq;
    
        @Column(name = "test_text")
        private String testText;
    
        public SingleAJpa() {}
    
        public SingleAJpa(String testText) {
            this.testText = testText;
        }
    
        public Long getSeq() {
            return seq;
        }
    
        public String getTestText() {
            return testText;
        }
    }

    SingleBJpa

    Single B DB에서 사용하는 도메인입니다.

    package com.datasource.domain.singleB;
    
    import javax.persistence.*;
    
    /**
     * <pre>
     *     JPA용 JTA 테스트를 위한 엔티티
     *     해당 엔티티는 Singel B(Postgresql) DB에서 사용.
     * </pre>
     */
    @Entity
    @Table(name = "single_b_jpa")
    public class SingleBJpa {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long seq;
    
        @Column(name = "test_text")
        private String testText;
    
        public SingleBJpa() {}
    
        public SingleBJpa(String testText) {
            this.testText = testText;
        }
    
        public Long getSeq() {
            return seq;
        }
    
        public String getTestText() {
            return testText;
        }
    }

    Service

    서비스 영역 코드는 테스트 코드이기 때문에 별도의 설명 없이 코드만 작성하도록 하겠습니다.

    단일 트랜잭션

    • SingleServiceImpl

    Mybatis용 서비스 테스트 코드 입니다.

    package com.datasource.service.single;
    
    import com.datasource.repo.mybatis.single.singleA.SingleAMapper;
    import com.datasource.repo.mybatis.single.singleB.SingleBMapper;
    import com.datasource.spring.config.annotaion.SingleATransactional;
    import com.datasource.spring.config.annotaion.SingleBTransactional;
    import org.springframework.stereotype.Service;
    
    /**
     * <pre>
     *     Mybatis를 활용한 JTA 트랜잭션이 아닌 단일 트랜잭션을 확인하기 위핸 테스트 클래스.
     * </pre>
     */
    @Service
    public class SingleServiceImpl {
        private final SingleAMapper singleAMapper;
        private final SingleBMapper singleBMapper;
    
        public SingleServiceImpl(SingleAMapper singleAMapper, SingleBMapper singleBMapper) {
            this.singleAMapper = singleAMapper;
            this.singleBMapper = singleBMapper;
        }
    
        /**********************/
        /*Single A 트랜잭션 영역*/
        /**********************/
        /**
         * <pre>
         *     SingleA 단일 트랜잭션 정상 저장을 확인하기 위한 메서드.
         * </pre>
         */
        @SingleATransactional(rollbackFor = {Exception.class})
        public long singleASaveTest(String testText) {
            return this.singleAMapper.saveTest(testText);
        }
    
        /**
         * <pre>
         *     SingleA 단일 트랜잭션 롤백을 확인하기 위한 메서드.
         * </pre>
         */
        @SingleATransactional(rollbackFor = {Exception.class})
        public long singleASaveRollbackTest(String testText) {
            this.singleAMapper.saveTest(testText);
    
            throw new RuntimeException("Single A Rollback Test");
        }
    
        /**
         * <pre>
         *     SingleA 단일 트랜잭션 정상 조회를 확인하기 위한 메서드.
         * </pre>
         */
        @SingleATransactional
        public String singleAFindTestText(String testText) {
            return this.singleAMapper.findTestText(testText);
        }
    
        /**********************/
        /*Single B 트랜잭션 영역*/
        /**********************/
        /**
         * <pre>
         *     SingleB 단일 트랜잭션 정상 저장을 확인하기 위한 메서드.
         * </pre>
         */
        @SingleBTransactional(rollbackFor = {Exception.class})
        public long singleBSaveTest(String testText) {
            return this.singleBMapper.saveTest(testText);
        }
    
        /**
         * <pre>
         *     SingleB 단일 트랜잭션 롤백을 확인하기 위한 메서드.
         * </pre>
         */
        @SingleBTransactional(rollbackFor = {Exception.class})
        public long singleBSaveRollbackTest(String testText) {
            this.singleBMapper.saveTest(testText);
    
            throw new RuntimeException("Single B Rollback Test");
        }
    
        /**
         * <pre>
         *     SingleB 단일 트랜잭션 정상 조회를 확인하기 위한 메서드.
         * </pre>
         */
        @SingleBTransactional
        public String singleBFindTestText(String testText) {
            return this.singleBMapper.findTestText(testText);
        }
    }
    • SingleJpaServiceImpl

    JPA용 서비스 테스트 코드 입니다.

    package com.datasource.service.single;
    
    import com.datasource.domain.singleA.SingleAJpa;
    import com.datasource.domain.singleB.SingleBJpa;
    import com.datasource.repo.jpa.single.singleA.SingleARepository;
    import com.datasource.repo.jpa.single.singleB.SingleBRepository;
    import com.datasource.spring.config.annotaion.SingleAJpaTransactional;
    import com.datasource.spring.config.annotaion.SingleBJpaTransactional;
    import org.springframework.stereotype.Service;
    
    import java.util.Optional;
    
    /**
     * <pre>
     *     JPA를 활용한 JTA 트랜잭션이 아닌 단일 트랜잭션을 확인하기 위핸 테스트 클래스.
     * </pre>
     */
    @Service
    public class SingleJpaServiceImpl {
        private final SingleARepository singleARepository;
        private final SingleBRepository singleBRepository;
    
        public SingleJpaServiceImpl(SingleARepository singleARepository, SingleBRepository singleBRepository) {
            this.singleARepository = singleARepository;
            this.singleBRepository = singleBRepository;
        }
    
        /**********************/
        /*Single A 트랜잭션 영역*/
        /**********************/
        /**
         * <pre>
         *     SingleA 단일 트랜잭션 정상 저장을 확인하기 위한 메서드.
         * </pre>
         */
        @SingleAJpaTransactional(rollbackFor = {Exception.class})
        public void singleASaveTest(SingleAJpa singleAJpa) {
            this.singleARepository.save(singleAJpa);
        }
    
        /**
         * <pre>
         *     SingleA 단일 트랜잭션 정상 롤백을 확인하기 위한 메서드.
         * </pre>
         */
        @SingleAJpaTransactional(rollbackFor = {Exception.class})
        public void singleARollbackTest(SingleAJpa singleAJpa) {
            this.singleARepository.save(singleAJpa);
    
            throw new RuntimeException("Single B Rollback Test");
        }
    
        /**
         * <pre>
         *     SingleA 단일 트랜잭션 정상 조회를 확인하기 위한 메서드.
         * </pre>
         */
        @SingleAJpaTransactional
        public Optional<SingleAJpa> singleAFindTestText(Long seq) {
            return this.singleARepository.findById(seq);
        }
    
        /**********************/
        /*Single B 트랜잭션 영역*/
        /**********************/
        /**
         * <pre>
         *     SingleB 단일 트랜잭션 정상 저장을 확인하기 위한 메서드.
         * </pre>
         */
        @SingleBJpaTransactional(rollbackFor = {Exception.class})
        public void singleBSaveTest(SingleBJpa singleBJpa) {
            this.singleBRepository.save(singleBJpa);
        }
    
        /**
         * <pre>
         *     SingleB 단일 트랜잭션 정상 롤백을 확인하기 위한 메서드.
         * </pre>
         */
        @SingleBJpaTransactional(rollbackFor = {Exception.class})
        public void singleBRollbackTest(SingleBJpa singleBJpa) {
            this.singleBRepository.save(singleBJpa);
    
            throw new RuntimeException("Single B Rollback Test");
        }
    
        /**
         * <pre>
         *     SingleB 단일 트랜잭션 정상 조회를 확인하기 위한 메서드.
         * </pre>
         */
        @SingleBJpaTransactional
        public Optional<SingleBJpa> singleBFindTestText(Long seq) {
            return this.singleBRepository.findById(seq);
        }
    }

    JTA 트랜잭션

    • JtaServiceImpl

    Mybatis용 JTA 서비스 테스트 코드 입니다.

    package com.datasource.service.jta;
    
    import com.datasource.repo.mybatis.jta.singleA.JtaSingleAMapper;
    import com.datasource.repo.mybatis.jta.singleB.JtaSingleBMapper;
    import com.datasource.spring.config.annotaion.JtaTransactional;
    import org.springframework.stereotype.Service;
    
    /**
     * <pre>
     *     Mybatis JTA 트랜잭션 확인을 위한 테스트 클래스.
     * </pre>
     */
    @Service
    public class JtaServiceImpl {
        private final JtaSingleAMapper jtaSingleAMapper;
        private final JtaSingleBMapper jtaSingleBMapper;
    
        public JtaServiceImpl(JtaSingleAMapper jtaSingleAMapper, JtaSingleBMapper jtaSingleBMapper) {
            this.jtaSingleAMapper = jtaSingleAMapper;
            this.jtaSingleBMapper = jtaSingleBMapper;
        }
    
        /**
         * <pre>
         *     이기종 간(SingleA, SingelB)의 정상 트랜잭션을 확인 하기 위한 메서드.
         * </pre>
         */
        @JtaTransactional(rollbackFor = {Exception.class})
        public long saveTest(String testText) {
            return this.jtaSingleAMapper.saveTest(testText) + this.jtaSingleBMapper.saveTest(testText);
        }
    
        /**
         * <pre>
         *     이기종 간(SingleA, SingleB)의 트랜잭션 롤백을 확인 하기 위한 메서드.
         * </pre>
         */
        @JtaTransactional(rollbackFor = {Exception.class})
        public void saveRollbackTest(String testText) {
            this.jtaSingleAMapper.saveTest(testText);
            this.jtaSingleBMapper.saveTest(testText);
    
            if(true) {
                throw new RuntimeException("Rollback Test Exception");
            }
        }
    }
    • JpaJtaServiceImpl

    JPA용 JTA 서비스 테스트 코드 입니다.

    package com.datasource.service.jta;
    
    import com.datasource.domain.singleA.SingleAJpa;
    import com.datasource.domain.singleB.SingleBJpa;
    import com.datasource.repo.jpa.jta.singleA.JtaSingleARepository;
    import com.datasource.repo.jpa.jta.singleB.JtaSingleBRepository;
    import com.datasource.spring.config.annotaion.JtaTransactional;
    import org.springframework.stereotype.Service;
    
    /**
     * <pre>
     *     JPA JTA 트랜잭션 확인을 위한 테스트 클래스.
     * </pre>
     */
    @Service
    public class JpaJtaServiceImpl {
        private final JtaSingleARepository jtaSingleARepository;
        private final JtaSingleBRepository jtaSingleBRepository;
    
        public JpaJtaServiceImpl(JtaSingleARepository jtaSingleARepository, JtaSingleBRepository jtaSingleBRepository) {
            this.jtaSingleARepository = jtaSingleARepository;
            this.jtaSingleBRepository = jtaSingleBRepository;
        }
    
        /**
         * <pre>
         *     이기종 간(SingleA, SingelB)의 정상 트랜잭션을 확인 하기 위한 메서드.
         * </pre>
         */
        @JtaTransactional(rollbackFor = {Exception.class})
        public void saveTest(SingleAJpa singleAJpa, SingleBJpa singleBJpa) {
            this.jtaSingleARepository.save(singleAJpa);
            this.jtaSingleBRepository.save(singleBJpa);
        }
    
        /**
         * <pre>
         *     이기종 간(SingleA, SingleB)의 트랜잭션 롤백을 확인 하기 위한 메서드.
         * </pre>
         */
        @JtaTransactional(rollbackFor = {Exception.class})
        public void saveRollbackTest(SingleAJpa singleAJpa, SingleBJpa singleBJpa) {
            this.jtaSingleARepository.save(singleAJpa);
            this.jtaSingleBRepository.save(singleBJpa);
    
            if(true) {
                throw new RuntimeException("Rollback Test Exception");
            }
        }
    }

    테스트

    JTA 로그를 면밀하게 살펴 보기 위해 logback 설정을 아래와 같이 하였습니다. 로그 설정 중 CompositeTransactionImp 클래스 부분이 JTA 트랜잭션을 관리하는 클래스 입니다. 실제로 Commit과 Rollback 같은 행위는 다른 클래스들이 담당하지만 최종적으론 해당 클래스를 통해 Commit과 Rollback 등이 수행 됩니다.

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
        <appender name="LogToConsole" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread{10}] %logger{0}\(%line\) - %msg %n </pattern>
            </encoder>
        </appender>
    
        <logger name="org.springframework.transaction.jta.JtaTransactionManager" level="debug"/>
    
        <logger name="com.atomikos.jdbc.AtomikosDataSourceBean" level="debug"/>
        <logger name="com.atomikos.jdbc.AbstractDataSourceBean" level="debug"/>
        <logger name="com.atomikos.jdbc.AtomikosConnectionProxy" level="debug"/>
        <logger name="com.atomikos.icatch.imp.CompositeTransactionImp" level="debug"/>
    
        <root level="info">
            <appender-ref ref="LogToConsole"/>
        </root>
    </configuration>

    그리고 예제 프로젝트 내에 jta.properties 파일이 존재하는데 해당 파일이 Atomikos의 설정 파일 입니다. 설정 파일은 프로젝트의 최상위 경로에 위치해야 하며 Atomikos 설정 파일이 없으면 기본으로 라이브러리에 내장되어 있는 transactions-defaults.properties 파일을 참조하게 됩니다.

    Atomikos는 아래 파일명의 설정 파일들을 찾게 되고 각 설정 파일마다 적용되는 우선 순위가 존재합니다. 우선 순위에 따라 설정 값들이 추가 되거나 덮어쓰기가 됩니다.

    1. transactions-defaults.properties 라이브러리에 있는 기본 설정 파일
    2. transactions.properties
    3. jta.properties
    4. JVM (System) Properties Java 실행 시 -Dcom.atomikos.icatch.file 매개변수에 위치한 properties 파일

    Atomikos의 AssemblerImp 구현체를 확인해보면 아래와 같이 구현되어 있습니다.

    public class AssemblerImp implements Assembler {
        private static final String DEFAULT_PROPERTIES_FILE_NAME = "transactions-defaults.properties";
    
        private static final String JTA_PROPERTIES_FILE_NAME = "jta.properties";
    
        private static final String TRANSACTIONS_PROPERTIES_FILE_NAME = "transactions.properties";
    
        .
        .
        .
    
        @Override
            public ConfigProperties initializeProperties() {
                Properties defaults = new Properties();
                // 첫 번째로 라이브러리 내에 존재하는 transactions-defaults.properties 파일을 로드.
                loadPropertiesFromClasspath(defaults, DEFAULT_PROPERTIES_FILE_NAME);
                Properties transactionsProperties = new Properties(defaults);
    
                // 두 번째로 프로젝트 최상위 경로의 transactions.properties 파일을 로드해서 설정을 추가하거나 덮어쓰기.
                loadPropertiesFromClasspath(transactionsProperties, TRANSACTIONS_PROPERTIES_FILE_NAME);
                Properties jtaProperties = new Properties(transactionsProperties);
    
                // 세 번째로 프로젝트 최상위 경로의 jta.properties 파일을 로드해서 설정을 추가하거나 덮어쓰기.
                loadPropertiesFromClasspath(jtaProperties, JTA_PROPERTIES_FILE_NAME);
                Properties customProperties = new Properties(jtaProperties);
    
                // 네 번째로 java 실행 시 -Dcom.atomikos.icatch.file 매개 변수에 위치한 properties 파일을 로드해서 설정을 추가하거나 덮어쓰기.
                loadPropertiesFromCustomFilePath(customProperties);
                Properties finalProperties = new Properties(customProperties);
                ConfigProperties configProperties = new ConfigProperties(finalProperties);
                checkRegistration(configProperties);
                return configProperties;
        }
    
        .
        .
        .
    }

    Mybatis

    단일 트랜잭션

    • 정상 저장 테스트

    단일 트랜잭션으로 되어 있는 정상 저장 테스트 입니다.

    @Test
        @DisplayName("단일 트랜잭션 정상 저장 테스트")
        void single_datasource_insert_test() {
            String saveAndFindValue = "single_test_value";
    
            long singleASeq = this.singleServiceImpl.singleASaveTest(saveAndFindValue);
            long singleBSeq = this.singleServiceImpl.singleBSaveTest(saveAndFindValue);
    
            logger.info("singleASeq : {}, singleBSeq : {}", singleASeq, singleBSeq);
    
            Assertions.assertTrue(singleASeq > 0 && singleBSeq > 0);
        }

    결과

    큰 문제 없이 정상 저장 되었음을 알 수 있습니다.

    JtaApplicationTests(71) - singleASeq : 1, singleBSeq : 1
    • 정상 롤백 테스트

    JTA를 사용한 트랜잭션이 아니기 때문에 각 Single A, B 트랜잭션들이 정상적으로 롤백이 되는지를 확인하였습니다.

    @Test
    @DisplayName("단일 트랜잭션 정상 롤백 테스트")
    void single_datasource_rollback_test() {
        String saveAndFindValue = "single_rollback_test_value";
    
        Assertions.assertThrowsExactly(RuntimeException.class, () -> {
            this.singleServiceImpl.singleASaveRollbackTest(saveAndFindValue);
        });
    
        Assertions.assertThrowsExactly(RuntimeException.class, () -> {
            this.singleServiceImpl.singleBSaveRollbackTest(saveAndFindValue);
        });
    
        Assertions.assertTrue(null == this.singleServiceImpl.singleAFindTestText(saveAndFindValue)
                && null == this.singleServiceImpl.singleBFindTestText(saveAndFindValue));
    }

    결과

    별도 로그 출력은 없습니다.

    JTA 트랜잭션

    • 정상 저장 테스트

    JTA 트랜잭션을 사용해서 Single A, B에 정상으로 저장되는지 여부를 확인하는 테스트 입니다.

    @Test
    @DisplayName("JTA 트랜잭션 정상 저장 테스트")
    void jta_datasource_insert_test() {
        String saveAndFindValue = "jta_test_value";
    
        logger.info("jta insert count : {}", this.jtaServiceImpl.saveTest(saveAndFindValue));
    
        // JTA 트랜잭션을 통해 정상적으로 저장되었는지 확인하기 위해 JTA 트랜잭션이 아닌 일반 트랜잭션으로 조회.
        Assertions.assertTrue(saveAndFindValue.equals(this.singleServiceImpl.singleAFindTestText(saveAndFindValue))
                && saveAndFindValue.equals(this.singleServiceImpl.singleBFindTestText(saveAndFindValue)));
    }

    결과

    Single A, B에 정상적으로 저장 되었음을 확인할 수 있습니다.

    JtaTransactionManager(370) - Creating new transaction with name [com.datasource.service.jta.JtaServiceImpl.saveTest]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'jtaSingleTransactionManager',-java.lang.Exception 
    TransactionServiceImp(24) - Attempt to create a transaction with a timeout that exceeds maximum - truncating to: 300000 
    AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleADataSource': getConnection()... 
    AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleADataSource': init... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: calling getAutoCommit... 
    CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132333535303930333030303031:3139322E3136382E312E3230372E746D31 ) for transaction 192.168.1.207.tm169812355090300001 
    CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@4ba4e4c1 ) for transaction 192.168.1.207.tm169812355090300001 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: calling prepareStatement(insert into test(
                test_text
            ) values (
                ?
            ))... 
    AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleBDataSource': getConnection()... 
    AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleBDataSource': init... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@51297528: calling getAutoCommit... 
    CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132333535303930333030303031:3139322E3136382E312E3230372E746D32 ) for transaction 192.168.1.207.tm169812355090300001 
    CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@4ba4e4c1 ) for transaction 192.168.1.207.tm169812355090300001 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@51297528: calling prepareStatement(insert into test(
                test_text
            ) values (
                ?
            ))... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: close()... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@51297528: close()... 
    JtaTransactionManager(740) - Initiating transaction commit 
    CompositeTransactionImp(32) - commit() done (by application) of transaction 192.168.1.207.tm169812355090300001 
    JtaApplicationTests(98) - jta insert count : 2

    위 로그를 보면 아래와 같은 형태로 각 DB 작업마다 수행 했음을 알 수 있습니다.

    AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleADataSource': getConnection()... 
    AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleADataSource': init... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: calling getAutoCommit... 
    CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132333535303930333030303031:3139322E3136382E312E3230372E746D31 ) for transaction 192.168.1.207.tm169812355090300001 
    CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@4ba4e4c1 ) for transaction 192.168.1.207.tm169812355090300001 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: calling prepareStatement(insert into test(
                test_text
            ) values (
                ?
            ))... 

    그리고 마지막으로 아래와 같이 성공적으로 트랜잭션이 Commit 되었음을 확인할 수 있습니다.

    JtaTransactionManager(740) - Initiating transaction commit 
    CompositeTransactionImp(32) - commit() done (by application) of transaction 192.168.1.207.tm169812355090300001 
    • 정상 롤백 테스트

    JTA 트랜잭션을 사용해서 Single A, B에 정상으로 롤백이 되었는지 확인하는 테스트 입니다.

    @Test
    @DisplayName("JTA 트랜잭션 정상 롤백 테스트")
    void jta_rollback_test() {
        String saveAndFindValue = "jta_rollback_test_value";
    
        Assertions.assertThrowsExactly(RuntimeException.class, () -> {
            this.jtaServiceImpl.saveRollbackTest(saveAndFindValue);
        });
    
        Assertions.assertTrue(null == this.singleServiceImpl.singleAFindTestText(saveAndFindValue)
                && null == this.singleServiceImpl.singleBFindTestText(saveAndFindValue));
    }

    결과

    Single A, B에 정상적으로 롤백이 되었음을 확인할 수 있습니다.

    JtaTransactionManager(370) - Creating new transaction with name [com.datasource.service.jta.JtaServiceImpl.saveRollbackTest]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'jtaSingleTransactionManager',-java.lang.Exception 
    TransactionServiceImp(24) - Attempt to create a transaction with a timeout that exceeds maximum - truncating to: 300000 
    AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleADataSource': getConnection()... 
    AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleADataSource': init... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: calling getAutoCommit... 
    CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132333535313634323030303032:3139322E3136382E312E3230372E746D33 ) for transaction 192.168.1.207.tm169812355164200002 
    CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@7a29e18b ) for transaction 192.168.1.207.tm169812355164200002 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: calling prepareStatement(insert into test(
                test_text
            ) values (
                ?
            ))... 
    AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleBDataSource': getConnection()... 
    AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleBDataSource': init... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@51297528: calling getAutoCommit... 
    CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132333535313634323030303032:3139322E3136382E312E3230372E746D34 ) for transaction 192.168.1.207.tm169812355164200002 
    CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@7a29e18b ) for transaction 192.168.1.207.tm169812355164200002 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@51297528: calling prepareStatement(insert into test(
                test_text
            ) values (
                ?
            ))... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: close()... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@51297528: close()... 
    JtaTransactionManager(833) - Initiating transaction rollback 
    CompositeTransactionImp(32) - rollback() done of transaction 192.168.1.207.tm169812355164200002 
    CompositeTransactionImp(32) - rollback() done of transaction 192.168.1.207.tm169812355164200002

    아래를 보면 정상적으로 롤백이 수행되었음 알 수 있습니다.

    JtaTransactionManager(833) - Initiating transaction rollback 
    CompositeTransactionImp(32) - rollback() done of transaction 192.168.1.207.tm169812355164200002 
    CompositeTransactionImp(32) - rollback() done of transaction 192.168.1.207.tm169812355164200002

    여기서 잠시 JPA 테스트를 확인하기 전에 추가적으로 설명 드릴 부분이 있는데 Atomikos는 JTA 트랜잭션 처리를 위해 트랜잭션 로그 파일을 생성합니다. 이 트랜잭션 로그 파일은 개발자가 제어할 수 없는 부분이며 해당 파일에는 트랜잭션 로그와 기타 트랜잭션 관리에 필요한 정보가 저장됩니다. 이 트랜잭션 로그 파일를 가지고 트랜잭션 롤백 및 복구와 같은 중요한 작업을 수행하게 됩니다.

    다시 위 테스트 로그 중 트랜잭션 로그 파일과 관련된 로그를 보면 192.168.1.207.tm169812355164200002 와 같은 형태로 각 DB 작업이 어떤 트랜잭션에 묶여있는지 확인할 수 있습니다.

    -- Single A DB
    addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132333535313634323030303032:3139322E3136382E312E3230372E746D33 ) for transaction 192.168.1.207.tm169812355164200002 
    registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@7a29e18b ) for transaction 192.168.1.207.tm169812355164200002 
    
    -- Single B DB
    addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132333535313634323030303032:3139322E3136382E312E3230372E746D34 ) for transaction 192.168.1.207.tm169812355164200002 
    registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@7a29e18b ) for transaction 192.168.1.207.tm169812355164200002

    그리고 해당 DB 작업들에 트랜잭션 결과도 192.168.1.207.tm169812355164200002 와 같은 형태로 어떤게 수행 되었는지 확인할 수 있습니다.

    rollback() done of transaction 192.168.1.207.tm169812355164200002

    트랜잭션 로그 파일의 내용을 확인해보면 아래와 같은 로그 정보가 포함되어 있습니다.(해당 부분은 Spring 2.7.16 버전이며 3.1.4 내용이 달라집니다.)

    -- Rollback인 경우
    {
        "id": "192.168.1.207.tm169830186293600001",
        "wasCommitted": false,
        "participants": [
            {
                "uri": "192.168.1.207.tm1",
                "state": "TERMINATED",
                "expires": 1698302163038,
                "resourceName": "jtaSingleADataSource"
            },
            {
                "uri": "192.168.1.207.tm2",
                "state": "TERMINATED",
                "expires": 1698302163038,
                "resourceName": "jtaSingleBDataSource"
            }
        ]
    }
    
    -- Commit인 경우
    {
        "id": "192.168.1.207.tm169830186308000002",
        "wasCommitted": true,
        "participants": [
            {
                "uri": "192.168.1.207.tm3",
                "state": "COMMITTING",
                "expires": 1698302163106,
                "resourceName": "jtaSingleADataSource"
            },
            {
                "uri": "192.168.1.207.tm4",
                "state": "COMMITTING",
                "expires": 1698302163106,
                "resourceName": "jtaSingleBDataSource"
            }
        ]
    }

    JPA

    단일 트랜잭션

    • 정상 저장 테스트

    JPA를 활용한 단일 트랜잭션 정상 저장 테스트 입니다.

    @Test
    @DisplayName("단일 트랜잭션 정상 저장 테스트")
    void single_jpa_datasource_insert_test() {
        String saveAndFindValue = "single_test_vale";
        SingleAJpa singleAJpa = new SingleAJpa(saveAndFindValue);
        SingleBJpa singleBJpa  = new SingleBJpa(saveAndFindValue);
    
        this.singleJpaServiceImpl.singleASaveTest(singleAJpa);
        this.singleJpaServiceImpl.singleBSaveTest(singleBJpa);
    
        long singleASeq = singleAJpa.getSeq();
        long singleBSeq = singleBJpa.getSeq();
    
        logger.info("singleASeq : {}, singleBSeq : {}", singleASeq, singleBSeq);
    
        Assertions.assertTrue(singleASeq > 0 && singleBSeq > 0);
    
    }

    결과

    정상으로 저장되었음을 확인할 수 있습니다.

    Hibernate: 
        insert 
        into
            single_a_jpa
            (test_text) 
        values
            (?)
    Hibernate: 
        insert 
        into
            single_b_jpa
            (test_text) 
        values
            (?)
    JtaJpaApplicationTests(38) - singleASeq : 3, singleBSeq : 3
    • 정상 롤백 테스트

    롤백 테스트도 위 Mybatis 롤백 테스트와 동일하게 각 단일 트랜잭션에 대한 롤백 테스트를 하였습니다.

    @Test
    @DisplayName("단일 트랜잭션 정상 롤백 테스트")
    void single_jpa_datasource_rollback_test() {
        String saveAndFindValue = "single_test_vale";
        SingleAJpa singleAJpa = new SingleAJpa(saveAndFindValue);
        SingleBJpa singleBJpa  = new SingleBJpa(saveAndFindValue);
    
        Assertions.assertThrowsExactly(RuntimeException.class, () -> this.singleJpaServiceImpl.singleARollbackTest(singleAJpa));
        Assertions.assertThrowsExactly(RuntimeException.class, () -> this.singleJpaServiceImpl.singleBRollbackTest(singleBJpa));
    
        long singleASeq = singleAJpa.getSeq();
        long singleBSeq = singleBJpa.getSeq();
    
        Assertions.assertTrue(this.singleJpaServiceImpl.singleAFindTestText(singleASeq).isEmpty()
                && this.singleJpaServiceImpl.singleBFindTestText(singleBSeq).isEmpty());
    }

    결과

    각 단일 트랜잭션 롤백에 대한 테스트도 정상임을 알 수 있습니다.

    Hibernate: 
        insert 
        into
            single_a_jpa
            (test_text) 
        values
            (?)
    Hibernate: 
        insert 
        into
            single_b_jpa
            (test_text) 
        values
            (?)
    Hibernate: 
        select
            singleajpa0_.seq as seq1_0_0_,
            singleajpa0_.test_text as test_tex2_0_0_ 
        from
            single_a_jpa singleajpa0_ 
        where
            singleajpa0_.seq=?
    Hibernate: 
        select
            singlebjpa0_.seq as seq1_0_0_,
            singlebjpa0_.test_text as test_tex2_0_0_ 
        from
            single_b_jpa singlebjpa0_ 
        where
            singlebjpa0_.seq=?

    JTA 트랜잭션

    • 정상 저장 테스트

    JTA 트랜잭션을 사용한 저장 테스트 입니다.

    @Test
    @DisplayName("JTA 트랜잭션 정상 저장 테스트")
    void jta_jpa_datasource_insert_test() {
        String saveAndFindValue = "jta_test_value";
        SingleAJpa singleAJpa = new SingleAJpa(saveAndFindValue);
        SingleBJpa singleBJpa  = new SingleBJpa(saveAndFindValue);
    
        this.jpaJtaServiceImpl.saveTest(singleAJpa, singleBJpa);
    
        long singleASeq = singleAJpa.getSeq();
        long singleBSeq = singleBJpa.getSeq();
    
        logger.info("singleASeq : {}, singleBSeq : {}", singleASeq, singleBSeq);
    
        // JTA 트랜잭션을 통해 정상적으로 저장되었는지 확인하기 위해 JTA 트랜잭션이 아닌 일반 트랜잭션으로 조회.
        Assertions.assertTrue(saveAndFindValue.equals(this.singleJpaServiceImpl.singleAFindTestText(singleASeq).get().getTestText())
                && saveAndFindValue.equals(this.singleJpaServiceImpl.singleBFindTestText(singleBSeq).get().getTestText()));
    }

    결과

    CompositeTransactionImp(32) - commit() done (by application) of transaction 192.168.1.207.tm169812958131600002 출력으로 보아 JTA 트랜잭션을 사용해서 Single A, B 둘 다 정상 Commit을 확인할 수 있습니다.

    JtaTransactionManager(370) - Creating new transaction with name [com.datasource.service.jta.JpaJtaServiceImpl.saveTest]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'jtaSingleTransactionManager',-java.lang.Exception 
    TransactionServiceImp(24) - Attempt to create a transaction with a timeout that exceeds maximum - truncating to: 300000 
    JtaTransactionManager(470) - Participating in existing transaction 
    CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.icatch.jta.Sync2Sync@796e2187 ) for transaction 192.168.1.207.tm169812958131600002 
    AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleADataSource': getConnection()... 
    AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleADataSource': init... 
    CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132393538313331363030303032:3139322E3136382E312E3230372E746D33 ) for transaction 192.168.1.207.tm169812958131600002 
    CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@b5ad3f3e ) for transaction 192.168.1.207.tm169812958131600002 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: calling prepareStatement(insert into single_a_jpa (test_text) values (?),1)... 
    JtaTransactionManager(470) - Participating in existing transaction 
    CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.icatch.jta.Sync2Sync@59db8216 ) for transaction 192.168.1.207.tm169812958131600002 
    AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleBDataSource': getConnection()... 
    AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleBDataSource': init... 
    CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132393538313331363030303032:3139322E3136382E312E3230372E746D34 ) for transaction 192.168.1.207.tm169812958131600002 
    CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@b5ad3f3e ) for transaction 192.168.1.207.tm169812958131600002 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: calling prepareStatement(insert into single_b_jpa (test_text) values (?),1)... 
    JtaTransactionManager(740) - Initiating transaction commit 
    CompositeTransactionImp(32) - commit() done (by application) of transaction 192.168.1.207.tm169812958131600002 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: isClosed()... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: calling getWarnings... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: calling clearWarnings... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: close()... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: isClosed()... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: calling getWarnings... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: calling clearWarnings... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: close()... 
    JtaJpaApplicationTests(73) - singleASeq : 4, singleBSeq : 4 
    Hibernate: 
        select
            singleajpa0_.seq as seq1_0_0_,
            singleajpa0_.test_text as test_tex2_0_0_ 
        from
            single_a_jpa singleajpa0_ 
        where
            singleajpa0_.seq=?
    Hibernate: 
        select
            singlebjpa0_.seq as seq1_0_0_,
            singlebjpa0_.test_text as test_tex2_0_0_ 
        from
            single_b_jpa singlebjpa0_ 
        where
            singlebjpa0_.seq=?
    • 정상 롤백 테스트

    JTA 트랜잭션을 사용한 롤백 테스트 입니다.

    @Test
    @DisplayName("JTA 트랜잭션 정상 롤백 테스트")
    void jta_jpa_rollback_test() {
        String saveAndFindValue = "jta_rollback_test_value";
    
        Assertions.assertThrowsExactly(RuntimeException.class, () -> {
            SingleAJpa singleAJpa = new SingleAJpa(saveAndFindValue);
            SingleBJpa singleBJpa  = new SingleBJpa(saveAndFindValue);
    
            this.jpaJtaServiceImpl.saveRollbackTest(singleAJpa, singleBJpa);
        });
    }

    결과

    마찬가지로 실행 로그에 CompositeTransactionImp(32) - rollback() done of transaction 192.168.1.207.tm169812958119800001 출력된 것으로 롤백 되었음을 확인할 수 있습니다.

    JtaTransactionManager(370) - Creating new transaction with name [com.datasource.service.jta.JpaJtaServiceImpl.saveRollbackTest]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'jtaSingleTransactionManager',-java.lang.Exception 
    TransactionServiceImp(24) - Attempt to create a transaction with a timeout that exceeds maximum - truncating to: 300000 
    JtaTransactionManager(470) - Participating in existing transaction 
    CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.icatch.jta.Sync2Sync@7fdf7359 ) for transaction 192.168.1.207.tm169812958119800001 
    AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleADataSource': getConnection()... 
    AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleADataSource': init... 
    CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132393538313139383030303031:3139322E3136382E312E3230372E746D31 ) for transaction 192.168.1.207.tm169812958119800001 
    CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@908670c5 ) for transaction 192.168.1.207.tm169812958119800001 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: calling prepareStatement(insert into single_a_jpa (test_text) values (?),1)... 
    JtaTransactionManager(470) - Participating in existing transaction 
    CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.icatch.jta.Sync2Sync@24536f07 ) for transaction 192.168.1.207.tm169812958119800001 
    AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleBDataSource': getConnection()... 
    AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleBDataSource': init... 
    CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132393538313139383030303031:3139322E3136382E312E3230372E746D32 ) for transaction 192.168.1.207.tm169812958119800001 
    CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@908670c5 ) for transaction 192.168.1.207.tm169812958119800001 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: calling prepareStatement(insert into single_b_jpa (test_text) values (?),1)... 
    JtaTransactionManager(833) - Initiating transaction rollback 
    CompositeTransactionImp(32) - rollback() done of transaction 192.168.1.207.tm169812958119800001 
    AbstractConnectionProxy(24) - Forcing close of pending statement: Pooled statement wrapping physical statement null 
    AbstractConnectionProxy(24) - Forcing close of pending statement: Pooled statement wrapping physical statement null 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: isClosed()... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: calling getWarnings... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: calling clearWarnings... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: close()... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: isClosed()... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: calling getWarnings... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: calling clearWarnings... 
    AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: close()... 
    CompositeTransactionImp(32) - rollback() done of transaction 192.168.1.207.tm169812958119800001

    Spring boot 3.1.4

    위 내용까지가 Spring boot 2.7.16 버전에 대한 예제였습니다. 그럼 이번에는 위에서 언급 한 대로 Spring boot 3.1.4에 대한 예제를 설명하도록 하겠습니다. 해당 예제에 대한 테스트 구성은 위 Spring boot 2.7.16과 같기에 자세한 설명은 생략하고 변경 점에 관해서만 설명하도록 하겠습니다.

    build.gradle

    2.7.16

    build.gradle

    plugins {
        id 'java'
        id 'org.springframework.boot' version '2.7.16' // 변경 전
        id 'io.spring.dependency-management' version '1.1.3'
    }
    
    dependencies {
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        runtimeOnly 'org.postgresql:postgresql'
    
        implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.3.1'
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.springframework.boot:spring-boot-starter-jta-atomikos' // 변경 전
    }

    3.1.4

    build.gradle

    plugins {
        id 'java'
        id 'org.springframework.boot' version '3.1.4' // 변경 후
        id 'io.spring.dependency-management' version '1.1.3'
    }
    
    dependencies {
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        runtimeOnly 'org.postgresql:postgresql'
    
        implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.3.1'
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'com.atomikos:transactions-spring-boot3-starter:6.0.0' // 변경 후
    }

    2.7.16은 Spring에서 제공하는 spring-boot-starter-jta-atomikos 를 의존하였지만 3.1.4에선 Spring이 아닌 Atomikos에서 제공하는 transactions-spring-boot3-starter:6.0.0 을 의존해야 합니다. Atomikos에서 JakartaEE 관련 대응한 라이브러리 버전이 6.0.0인데 spring-boot-starter-jta-atomikos 최신 버전인 2.7.16에선 JakartaEE에 대응되지 않는 Atomikos 4.0.x를 의존하기 때문입니다. Spring의 spring-boot-starter-jta-atomikos 의존이 Atomikos 6.0.0을 의존하기 전까지는 Atomikos의 transactions-spring-boot3-starter:6.0.0 를 의존해야 할 듯합니다.

    AtomikosDataSourceBean

    2.7.16의 spring-boot-2.7.16.jar에는 Atomikos에 대해 org.springframework.boot.jta.atomikos로 기본 내장되어 있었으나 3.1.4부터는 제외 되었습니다. 그래도 다행이라면 다행인게 위에서 언급한 Atomikos의 transactions-spring-boot3-starter:6.0.0 에서 기본으로 내장되어 있던 클래스들을 지원하고 있습니다. 그러나 문제가 될 수 있는 부분이 같은 클래스명이 다른 패키지에 존재하고 있기 때문에 import를 잘못하면 의도치 않게 동작할 수 있습니다.

    AtomikosDataSourceBean 클래스가 대표적인 예인데 해당 클래스는 com.atomikos.jdbc 패키지에도 있고 com.atomikos.spring 패키지에도 있습니다. 만약 import 할 때 com.atomikos.spring 패키지가 아닌 com.atomikos.jdbc 패키지를 의존하게 된다면 application.yml(xml)에 설정한 Atomikos 설정 내용을 정상 적용할 수 없습니다.

    즉 import를 할 때 패키지를 유심하게 살피셔야 합니다.

    2.7.16

    JtaSingleADatasource , JtaSingleBDatasource 클래스

    import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;

    3.1.4

    JtaSingleADatasource , JtaSingleBDatasource 클래스

    import com.atomikos.spring.AtomikosDataSourceBean;

    JakartaEE로 인한 패키지명 변경

    JavaEE에서 JakartaEE로 변경되면서 import 부분도 변경 되었습니다.

    2.7.16

    JtaSingleConfig 클래스

    import javax.transaction.TransactionManager;
    import javax.transaction.UserTransaction;

    SingleAJpa , SingleBJpa 클래스

    import javax.persistence.*;

    3.1.4

    JtaSingleConfig 클래스

    import jakarta.transaction.TransactionManager;
    import jakarta.transaction.UserTransaction;

    SingleAJpa , SingleBJpa 클래스

    import jakarta.persistence.*;

    Hibernate-Dialect

    해당 부분은 Postgresql에 한정입니다.

    2.7.16에서 의존하는 hibernate-core-5.6.15.Final.jar의 Postgresql Dialect 클래스가 아래와 같이 있었으나

    • PostgreSQL9Dialect
    • PostgreSQL10Dialect
    • PostgreSQL81Dialect
    • PostgreSQL82Dialect
    • PostgreSQL91Dialect
    • PostgreSQL92Dialect
    • PostgreSQL93Dialect
    • PostgreSQL94Dialect
    • PostgreSQL95Dialect

    3.1.4에서 의존하는 hibernate-core-6.2.9.Final.jar에서는 위 클래스들이 사라지고

    • PostgreSQLDialect

    와 같이 변경되었기에 hibernate.dialect 부분도 변경되었습니다.

    2.7.16

    JtaJpaSingleAConfig , PostgresqlSingleAJpaConfig, JtaJpaSingleBConfig , PostgresqlSingleBJtaJpaConfig 클래스

    properties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQL95Dialect");

    3.1.4

    JtaJpaSingleAConfig , PostgresqlSingleAJpaConfig, JtaJpaSingleBConfig , PostgresqlSingleBJtaJpaConfig 클래스

    properties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");

    기타

    기타 부분은 수정이 필요한 부분이 아니기에 자세한 설명은 생략하겠습니다.

    • Atomikos 로그 형식 변경
    • Atomikos 트랜잭션 로그 파일에 저장 되는 형식 변경

    마치며

    요즘은 MSA 아키텍처를 많이 사용하는 추세이므로 MSA 구조상 하나의 도메인에는 하나의 DB만 사용하는 식이라 이처럼 하나의 애플리케이션에서 여러 DB에 접속하는 상황은 지양할 것이라고 생각합니다.

    그러나 MSA는 트랜잭션이 분산될 수밖에 없는 구조이기에 트랜잭션에 관련된 처리에 많이 고민해야 하는 부분도 존재하고 인프라 구축에 대한 난이도 및 비용에 대한 부분도 상당한 편입니다.

    만약 MSA는 아니더라도 도메인 기반 설계를 하고 싶고 도메인마다 DB가 다른 경우나 CQRS 패턴과 같이 메시지 큐를 활용해서 Command와 Query DB 동기화가 하기 어려운 상황이라면 JTA를 도입하는 것도 좋은 방안이 되지 않을까 생각합니다. (물론 이 경우는 둘 다 RDB여야 합니다. 현재 Nosql에 대한 부분은 지원이 안되는 걸로 알고 있으며 된다 하더라도 확인은 못 해봤습니다.)

    마지막으로 해당 예제 프로젝트의 링크는 버전 별로 2.7.16, 3.1.4에서 확인할 수 있습니다. 마지막으로 긴 글이라면 긴 글을 끝까지 읽어주셔서 감사드리고 하시는 개발에 항상 좋은 성과만 있기를 기도드리겠습니다.

    감사합니다.

    반응형

    'Spring' 카테고리의 다른 글

    Spring Cloud Data Flow  (1) 2024.10.02
    Spring과 Builder Pattern  (0) 2023.04.05
    Spring과 Abstract Factory Pattern  (0) 2023.02.01
    Spring과 Factory Pattern  (1) 2023.01.19
    Spring과 Factory Method Pattern  (0) 2023.01.18

    댓글

Designed by Tistory.