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 구성은 아래 이미지를 참고하시면 되겠습니다.
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 구성은 아래 이미지를 참고하시면 되겠습니다.
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를 사용하고 앞서 설명 했듯이 @MapperScan
의 basePackages
속성의 패키지 경로를 단일 트랜잭션의 패키지 경로와 다르게 설정해야 합니다.
- 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 트랜잭션 설정은 간단합니다. 위에서 설명 했듯이 UserTransaction
과 UserTransactionManager
인터페이스의 구현체들을 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는 아래 파일명의 설정 파일들을 찾게 되고 각 설정 파일마다 적용되는 우선 순위가 존재합니다. 우선 순위에 따라 설정 값들이 추가 되거나 덮어쓰기가 됩니다.
transactions-defaults.properties
라이브러리에 있는 기본 설정 파일transactions.properties
jta.properties
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에서 확인할 수 있습니다. 마지막으로 긴 글이라면 긴 글을 끝까지 읽어주셔서 감사드리고 하시는 개발에 항상 좋은 성과만 있기를 기도드리겠습니다.
감사합니다.