이전 게시글을 통해 하나의 스프링 애플리케이션에서 여러 데이터베이스에 붙어 JPA로 작업하는 방법을 알아보았습니다.
이전 게시글의 문제점은 TCC
를 지원하지 않는다는 점이였습니다. 왜냐, 물리적으로 다른 트랜잭션 매니저를 사용하기 때문에 각각의 트랜잭션 매니저는 서로의 작업 처리 결과를 알지 못합니다.
이들을 묶어 처리하고 싶다면 글로벌 트랜잭션을 설정하고 사용해주어야 합니다.
오늘은 Atomikos
를 이용해 글로벌 트랜잭션을 구현해보겠습니다.
참고로 2023.07.09 기준으로 Atomikos는 스프링 부트3.0부터 지원하지 않습니다. 스프링 부트 3을 지원하는 별도의 Atomikos 라이브러리가 있는 듯 합니다.
https://stackoverflow.com/questions/75949411/how-to-replace-atomikos-in-distributed-transaction-in-spring-boot-3
JtaTransactionManager
는 무엇이고 내부 동작원리에 대해 궁금하다면 해당 링크를 참고합니다.
https://d2.naver.com/helloworld/5812258
공식문서
spring boot starter atomikos application 설정내용 https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties.transaction
implementation 'org.springframework.boot:spring-boot-starter-jta-atomikos'
spring:
datasource:
one:
xa-properties:
url: jdbc:mysql://localhost:3306/test1?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC&allowPublicKeyRetrieval=true
user: root
password: root
xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
unique-resource-name: 'one'
test-query: SELECT 1
min-pool-size: 5
max-pool-size: 10
hibernate:
ddl-auto: update
naming:
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
two:
xa-properties:
url: jdbc:mysql://localhost:3307/test2?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC&allowPublicKeyRetrieval=true
user: root
password: root
xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
unique-resource-name: 'two'
min-pool-size: 2
max-pool-size: 2
hibernate:
ddl-auto: update
xa-properties
설정 가능 리스트를 알고싶다면 AtomikosDataSourceBean 열어봐서 참고합니다.
@Data
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public class DatabaseProperties {
private One one;
private Two two;
@Data
public static class One {
private XaProperties xaProperties;
private String xaDataSourceClassName;
private String uniqueResourceName;
private int maxPoolSize;
private Hibernate hibernate;
}
@Data
public static class Two {
private XaProperties xaProperties;
private String xaDataSourceClassName;
private String uniqueResourceName;
private int maxPoolSize;
private Hibernate hibernate;
}
@Data
public static class XaProperties {
private String url;
private String user;
private String password;
}
@Data
public static class Hibernate {
private String ddlAuto;
private Naming naming;
public static Map<String, Object> propertiesToMap(Hibernate hibernateProperties) {
Map<String, Object> properties = new HashMap<>();
properties.put("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName());
properties.put("javax.persistence.transactionType", "JTA");
if(hibernateProperties.getDdlAuto() != null) {
properties.put("hibernate.hbm2ddl.auto", hibernateProperties.getDdlAuto());
}
DatabaseProperties.Naming hibernateNaming = hibernateProperties.getNaming();
if(hibernateNaming != null) {
if (hibernateNaming.getImplicitStrategy() != null) {
properties.put("hibernate.implicit_naming_strategy", hibernateNaming.getImplicitStrategy());
}
if (hibernateNaming.getPhysicalStrategy() != null) {
properties.put("hibernate.physical_naming_strategy", hibernateNaming.getPhysicalStrategy());
}
}
return properties;
} }
@Data
public static class Naming {
private String implicitStrategy;
private String physicalStrategy;
}
}
설정파일을 객체에 매핑해 사용하기 위해 정의합니다.
@Configuration
@EnableTransactionManagement
public class XaDataSourceConfig {
public static final String TRANSACTION_MANAGER_BEAN_NAME = "jtaTransactionManager";
@Bean
public UserTransactionManager userTransactionManager() throws SystemException {
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setTransactionTimeout(1000);
userTransactionManager.setForceShutdown(true);
return userTransactionManager;
}
@Bean
public UserTransaction userTransaction() throws SystemException {
var userTransaction = new UserTransactionImp();
userTransaction.setTransactionTimeout(60000);
return userTransaction;
}
@Primary
@Bean(name = TRANSACTION_MANAGER_BEAN_NAME)
public JtaTransactionManager jtaTransactionManager(UserTransactionManager userTransactionManager, UserTransaction userTransaction) {
JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
jtaTransactionManager.setTransactionManager(userTransactionManager);
jtaTransactionManager.setUserTransaction(userTransaction);
return jtaTransactionManager;
}
}
글로벌 트랜잭션 매니저를 설정합니다.
이전 게시글과의 차이점은 트랜잭션 매니저를 별도로 선언해 사용하지 않고 위에서 선언한 공통된 트랜잭션 매니저를 사용한다는 것입니다.
또한 엔티티 매니저 properties에 JTA 관련 내용을 추가했습니다.
@Configuration
@EnableConfigurationProperties(DatabaseProperties.class)
@EnableJpaRepositories(
basePackages = {"com.example.javajpamultidatabaseexample.account", "com.example.javajpamultidatabaseexample.board"},
entityManagerFactoryRef = PersistenceDBOneConfiguration.ENTITY_MANAGER_BEAN_NAME,
transactionManagerRef = XaDataSourceConfiguration.TRANSACTION_MANAGER_BEAN_NAME)
public class PersistenceDBOneConfiguration {
public static final String ENTITY_MANAGER_BEAN_NAME = "oneDBEntityManager";
private static final String DATASOURCE_BEAN_NAME = "oneDataSource";
private static final String DATASOURCE_PROPERTIES_PREFIX = "spring.datasource.one";
private static final String HIBERNATE_PROPERTIES = "oneHibernateProperties";
@Primary
@Bean(name = ENTITY_MANAGER_BEAN_NAME)
public LocalContainerEntityManagerFactoryBean entityManager(EntityManagerFactoryBuilder builder, @Qualifier(DATASOURCE_BEAN_NAME) DataSource dataSource,
@Qualifier(HIBERNATE_PROPERTIES) DatabaseProperties.Hibernate hibernateProperties) {
return builder.dataSource(dataSource).packages("com.example.javajpamultidatabaseexample.account", "com.example.javajpamultidatabaseexample.board")
.persistenceUnit(ENTITY_MANAGER_BEAN_NAME)
.properties(DatabaseProperties.Hibernate.propertiesToMap(hibernateProperties)).build();
}
@Bean(name = HIBERNATE_PROPERTIES)
@ConfigurationProperties(DATASOURCE_PROPERTIES_PREFIX + ".hibernate")
public DatabaseProperties.Hibernate hibernateProperties() {
return new DatabaseProperties.Hibernate();
}
@Primary
@Bean(name = DATASOURCE_BEAN_NAME)
@ConfigurationProperties(prefix = DATASOURCE_PROPERTIES_PREFIX)
public DataSource dataSource() {
return new AtomikosDataSourceBean();
}
}
@Configuration
@EnableConfigurationProperties(DatabaseProperties.class)
@EnableJpaRepositories(
basePackages = {"com.example.javajpamultidatabaseexample.school"},
entityManagerFactoryRef = PersistenceDBTwoConfiguration.ENTITY_MANAGER_BEAN_NAME,
transactionManagerRef = XaDataSourceConfiguration.TRANSACTION_MANAGER_BEAN_NAME)
public class PersistenceDBTwoConfiguration {
public static final String ENTITY_MANAGER_BEAN_NAME = "twoDBEntityManager";
private static final String DATASOURCE_BEAN_NAME = "twoDataSource";
private static final String DATASOURCE_PROPERTIES_PREFIX = "spring.datasource.two";
private static final String HIBERNATE_PROPERTIES = "twoHibernateProperties";
@Bean(name = ENTITY_MANAGER_BEAN_NAME)
public LocalContainerEntityManagerFactoryBean entityManager(EntityManagerFactoryBuilder builder, @Qualifier(DATASOURCE_BEAN_NAME) DataSource dataSource,
@Qualifier(HIBERNATE_PROPERTIES) DatabaseProperties.Hibernate hibernateProperties) {
return builder.dataSource(dataSource).packages("com.example.javajpamultidatabaseexample.school").persistenceUnit(ENTITY_MANAGER_BEAN_NAME)
.properties(DatabaseProperties.Hibernate.propertiesToMap(hibernateProperties)).build();
}
@Bean(name = HIBERNATE_PROPERTIES)
@ConfigurationProperties(DATASOURCE_PROPERTIES_PREFIX + ".hibernate")
public DatabaseProperties.Hibernate hibernateProperties() {
return new DatabaseProperties.Hibernate();
}
@Bean(name = DATASOURCE_BEAN_NAME)
@ConfigurationProperties(prefix = DATASOURCE_PROPERTIES_PREFIX)
public DataSource dataSource() {
return new AtomikosDataSourceBean();
}
}
@Override
@Transactional
public void test() {
// db1에 해당하는 엔티티
// db2에 해당하는 엔티티
}
이전과 다른점은 모든 데이터베이스가 사용하는 트랜잭션 매니저는 글로벌 트랜잭션 매니저 하나이므로 굳이 지정해주지 않아도 된다는 것입니다.
내부적으로 Two Phase Commit
을 해주어 일관성을 보장해준다는 장점은 좋지만,, 이는 곧 성능저하로 이어집니다.
글로벌 트랜잭션을 사용해야한다면 성능저하 측면에서 괜찮은지 연구해보고 사용해야할 듯 합니다. (저희는 사용하지 않기로 했습니다..!)