대빵's Blog

Atomikos를 이용한 이기종 DB 트랜잭션(Springboot + Mybatis) - 2. Application 환경구성 및 샘플코드 본문

개발관련

Atomikos를 이용한 이기종 DB 트랜잭션(Springboot + Mybatis) - 2. Application 환경구성 및 샘플코드

bigzero 2019. 8. 26. 15:15
Atomikos를 이용한 이기종 DB 트랜잭션(Springboot + Mybatis) - 2. Application 환경구성 및 샘플코드
 
Springboot 및 Mybatis 를 위한 Config 설정
 
  • application.properties 정의
 
spring.jta.enabled=true
 
# DATASOURCE master => develop server [XXX_DB] (Single DB Transaction)
#spring.db.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver
#spring.db.datasource.jdbc-url=jdbc:sqlserver://메인DB서버IP:포트;databaseName=XXX_DB
spring.db.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.db.datasource.jdbc-url=jdbc:log4jdbc:sqlserver://메인DB서버IP:포트;databaseName=XXX_DB
spring.db.datasource.username=메인DB서버계정
spring.db.datasource.password=메인DB서버암호
 
# Multiple Transaction Define
spring.db1.datasource.xa-data-source-class-name=com.microsoft.sqlserver.jdbc.SQLServerXADataSource
spring.db1.datasource.xa-properties.server-name=멀티DB서버1번IP    # Local Transaction 을 사용하는 DBMS와 동일한 DB 이다.
spring.db1.datasource.xa-properties.port-number=포트
spring.db1.datasource.xa-properties.database-name=XXX_DB    #DBMS 마다 DB 스키마 지정방법이 다르다...주의!
spring.db1.datasource.xa-properties.user=멀티DB서버1번계정
spring.db1.datasource.xa-properties.password=멀티DB서버1번암호
 
# TEST => localhost
spring.db2.datasource.xa-data-source-class-name=org.postgresql.xa.PGXADataSource
spring.db2.datasource.xa-properties.server-name=멀티DB서버2번IP
spring.db2.datasource.xa-properties.port-number=포트
spring.db2.datasource.xa-properties.current-schema=xxx_interface    #DBMS 마다 DB 스키마 지정방법이 다르다...주의!
spring.db2.datasource.xa-properties.user=멀티DB서버2번계정
spring.db2.datasource.xa-properties.password=멀티DB서버2번암호
 
 
  • 일단 멀티가 아닌 Local Transaction 을 사용하기 위한 설정 - Primary를 설정해야 한다.
    •  Multi Transaction 사용하지 않는 Single Transaction DB Config 먼저 구성한다. 일반적인 @Transaction 사용하면 아래 Config 내용이 적용된다.(@Primary 때문에)
    • mapper1 는 이기종 DBMS 설정과 같이 사용할 것이기 때문에 같은 패키기 구조로 정의했다. 만일 Local Transaction 을 사용하는 DBMS 와 이기종 1,2 번이 모두 다른 DBMS 이면 mapper를 서로 다르게 설정해야 한다.
    • 만일 모든 DBMS를 동일한 쿼리를 사용하고 싶다면 굳이 mapper(쿼리)를 나누지 않아도 된다. 단, 헷갈리기 시작하면 답이 없으므로 DBMS 에 따라 Mapper를 나누는 것을 권장한다.
 
    • @Configuration
    • @EnableTransactionManagement
    • @MapperScan(basePackages="com.xxx.integration.**.mapper1", sqlSessionFactoryRef="dsSqlSessionFactory")
    • public class XxxDbConfig {
    •  
    •         private final Logger logger = LoggerFactory.getLogger(XxxDbConfig.class);
    •         
    •         public static final String DS_DATASOURCE = "dsDataSource";
    •         
    •         /* ================> data source 1 start */
    •         @ConfigurationProperties(prefix="spring.db.datasource")
    •         @Bean(name = DS_DATASOURCE)
    •         public DataSource dataSource() {
    •                 return DataSourceBuilder.create().build();
    •         }
    •         
    • //        Transaction 을 명시적으로 선언하여 이름을 불러서 사용하려면 enable 시켜야 됨.
    • //        Spring Global Tx 를 사용하려면 JTA 와 JNDI 를 사용해야 한다.
    • //        Local Tx 는 1개의 txManager 만 허용하므로 Prime 이 필요하다.
    •         @Primary
    •         @Bean(name = "dsTxManager")
    •         public PlatformTransactionManager transactionManager (@Qualifier(DS_DATASOURCE) DataSource dataSource){
    •                 return  new DataSourceTransactionManager(dataSource);
    •         }
    •         
    •         @Bean(name="dsSqlSessionFactory")
    •         public SqlSessionFactory sqlSessionFactory(@Qualifier(DS_DATASOURCE) DataSource dataSource) throws Exception {
    •             final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
    •             sessionFactory.setDataSource(dataSource);
    •             return sessionFactory.getObject();
    •         }
    • }
 
 
 
 
 
  • 이기종 트랜잭션 처리를 위한 멀티 DBMS 1번 설정
    • 서로 다른 쿼리 실행을 위한 첫번째 DB mybatis 설정(보통 Single DB 구성에 드라이버만 XA 변경되도록 한다.)
 
    • /**
    •  * <pre>
    •  * com.xxx.integration.config
    •  * XxxDb1Config.java
    •  * </pre>
    •  *
    •  * @Author : Heo Dae-Young
    •  * @Date   : 2019. 4. 22.
    •  * @Version : 1.0
    •  * 일단 서로 다른 두 종류의 DB 이기 때문에 쿼리 문장이 다를 수 있으므로 mapper 분리를 위해
    •  * mybatis mapper 를 폴더로 분리하고 sqlSessionFactory 를 분리한다.
    •  * sqlserver 가 jdbc-url 프로퍼티를 정상적으로 인식하지 않기 때문에 value annotation 을 사용하여 프로퍼티값을 읽어온다.
    •  *
    •  */
    • @Configuration
    • @EnableTransactionManagement
    • @MapperScan(basePackages="com.xxx.integration.**.mapper1", sqlSessionFactoryRef="ds1SqlSessionFactory")
    • public class XxxDb1Config {
    •         
    •         @Value("${spring.db1.datasource.xa-data-source-class-name}") String ds1XaDataSourceClassName;
    •         @Value("${spring.db1.datasource.xa-properties.user}") String ds1User;
    •         @Value("${spring.db1.datasource.xa-properties.password}") String ds1Password;
    •         @Value("${spring.db1.datasource.xa-properties.server-name}") String ds1ServerName;
    •         @Value("${spring.db1.datasource.xa-properties.port-number}") String ds1PortNumber;
    •         @Value("${spring.db1.datasource.xa-properties.database-name}") String ds1DatabaseName;
    •  
    •         private final Logger logger = LoggerFactory.getLogger(XxxDb1Config.class);
    •         
    •         public static final String DS1_DATASOURCE = "ds1DataSource";
    •         
    •         /* ================> data source 1 start */
    •         @Bean(name = DS1_DATASOURCE)
    •         public DataSource dataSource() {
    •                 AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
    •                 ds.setUniqueResourceName(DS1_DATASOURCE);
    •                 ds.setXaDataSourceClassName(ds1XaDataSourceClassName);
    •                 
    •                 Properties p = new Properties();
    •                 p.setProperty("user", ds1User);
    •                 p.setProperty("password", ds1Password);
    •                 p.setProperty("serverName", ds1ServerName);
    •                 p.setProperty("portNumber", ds1PortNumber);
    •                 p.setProperty("databaseName", ds1DatabaseName);
    •                 ds.setXaProperties (p);
    •                 
    •                 return ds;
    •         }
    •         
    •         @Bean(name="ds1SqlSessionFactory")
    •     public SqlSessionFactory sqlSessionFactory(@Qualifier(DS1_DATASOURCE) DataSource dataSource) throws Exception {
    •         final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
    •         sessionFactory.setDataSource(dataSource);
    •         return sessionFactory.getObject();
    •     }
    • }
 
  • 두번째 DB mybatis 설정 정보
    • /**
    •  * <pre>
    •  * com.xxx.integration.config
    •  * XxxlDb2Config.java
    •  * </pre>
    •  *
    •  * @Author : Heo Dae-Young
    •  * @Date   : 2019. 4. 22.
    •  * @Version : 1.0
    •  *
    •  * 일단 서로 다른 두 종류의 DB 이기 때문에 쿼리 문장이 다를 수 있으므로 mapper 분리를 위해
    •  * mybatis mapper 를 폴더로 분리하고 sqlSessionFactory 를 분리한다.
    •  *
    •  */
    • @Configuration
    • @EnableTransactionManagement
    • @MapperScan(basePackages="com.xxx.integration.**.mapper2",sqlSessionFactoryRef="ds2SqlSessionFactory")
    • public class XxxDb2Config {
    •         
    •         @Value("${spring.db2.datasource.xa-data-source-class-name}") String ds2XaDataSourceClassName;
    •         @Value("${spring.db2.datasource.xa-properties.user}") String ds2User;
    •         @Value("${spring.db2.datasource.xa-properties.password}") String ds2Password;
    •         @Value("${spring.db2.datasource.xa-properties.server-name}") String ds2ServerName;
    •         @Value("${spring.db2.datasource.xa-properties.port-number}") String ds2PortNumber;
    •         @Value("${spring.db2.datasource.xa-properties.current-schema}") String ds2CurrentSchema;
    •  
    •         private final Logger logger = LoggerFactory.getLogger(XxxDb2Config.class);
    •         
    •         public static final String DS2_DATASOURCE = "ds2DataSource";
    •                 
    •         /* ================> data source 2 start */
    •     @Bean(name=DS2_DATASOURCE)
    •     public DataSource dataSource() {
    •             AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
    •                 ds.setUniqueResourceName(DS2_DATASOURCE);
    •                 ds.setXaDataSourceClassName(ds2XaDataSourceClassName);
    •                 
    •                 Properties p = new Properties();
    •                 p.setProperty("user", ds2User);
    •                 p.setProperty("password", ds2Password);
    •                 p.setProperty("serverName", ds2ServerName);
    •                 p.setProperty("portNumber", ds2PortNumber);
    •                 p.setProperty("currentSchema", ds2CurrentSchema);
    •                 ds.setXaProperties (p);
    •                 
    •                 return ds;
    •     }
    •  
    •     @Bean(name="ds2SqlSessionFactory")
    •     public SqlSessionFactory sqlSessionFactory(@Qualifier(DS2_DATASOURCE) DataSource dataSource) throws Exception {
    •         final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
    •         sessionFactory.setDataSource(dataSource);
    •         return sessionFactory.getObject();
    •     }
    • }
 
  • mybatis spring 에게 위임한 PlatformTransactionManager Atomikos 변경한다.
    • 아래 마지막에 PlatformTransactionManger 로서 별도의 Bean 정의하여 JtaTransactionManger 리턴하게끔 설정하는 것이 핵심이다.
    • /**
    •  * <pre>
    •  * com.xxx.integration.config
    •  * XxxMultiTxConfig.java
    •  * </pre>
    •  *
    •  * @Author : Heo Dae-Young
    •  * @Date   : 2019. 4. 22.
    •  * @Version : 1.0
    •  * Multi Transaction 처리를 위한 정의
    •  * UserTransaction 과 AtomikosTransaction 을 두개로 정의하고 두개의 Transaction 을 PlatformTransactionManager 하나로 묶는다.
    •  */
    • @Configuration
    • @EnableTransactionManagement
    • public class XxxMultiTxConfig {
    •  
    •         private final Logger logger = LoggerFactory.getLogger(XxxMultiTxConfig.class);
    •  
    •         @Bean(name = "userTransaction")
    •         public UserTransaction userTransaction() throws Throwable {
    •                 UserTransactionImp userTransactionImp = new UserTransactionImp();
    •                 userTransactionImp.setTransactionTimeout(10000);
    •                 return userTransactionImp;
    •         }
    •  
    •         @Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
    •         public TransactionManager atomikosTransactionManager() throws Throwable {
    •                 UserTransactionManager userTransactionManager = new UserTransactionManager();
    •                 userTransactionManager.setForceShutdown(false);
    •                 return userTransactionManager;
    •         }
    •  
    •         @Bean(name = "multiTxManager")
    •         @DependsOn({ "userTransaction", "atomikosTransactionManager" })
    •         public PlatformTransactionManager transactionManager() throws Throwable {
    •                 UserTransaction userTransaction = userTransaction();
    •                 JtaTransactionManager manager = new JtaTransactionManager(userTransaction, atomikosTransactionManager());
    •                 return manager;
    •         }
    •  
    • }
    •  
 
  • 테스트 코드 - 이기종 Mapper 2개를 Service 에서 단일 트랜잭션으로 묶어준다.
  • 해당 Service는 JUNIT4.x 에서 테스트 했다.
  • 두개의 서로다른 Mapper @Transactional 에서 multiTxManager 라는 Transaction 으로 묶어준다.
 
    • @Service
    • public class CommonService {
    •         
    •         @Autowired CommonMapper1 sqlServerDao;
    •         @Autowired CommonMapper2 postgreDao;
    •         
    •         @Transactional(value="multiTxManager")
    •         public int insertTxTest() {
    •                 
    •                 int result = 0;
    •                 
    •                 List<Map<String, String>> dataMapList = new ArrayList<>();
    •                 Map<String, String> dataMap = null;
    •                 for(int i=0,j=10 ; i<j ; i++) {
    •                         dataMap = new HashMap<>();
    •                         dataMap.put("C1", i+":key");
    •                         dataMap.put("C2", i+":value");
    •                         dataMapList.add(dataMap);
    •                 }
    •                 
    •                 for(int i=0 ,j=10 ; i<j ; i++) {
    •                         result = result + sqlServerDao.insertTxTestSqlServer(dataMapList.get(i));
    •                         result = result + postgreDao.insertTxTestPostgre(dataMapList.get(i));
    •                 }
    •                 return result;
    •         }
    •  
    • }
 
  • 참고 이기종 DBMS 2번이 Postgre 가 아닌 오라클 인 경우
  • 오라클인 경우 property 에 ULR로 설정해야 한다.
 
    • @Configuration
    • @EnableTransactionManagement
    • @MapperScan(basePackages="com.xxx.integration.**.mapper2",sqlSessionFactoryRef="ds2SqlSessionFactory")
    • public class XxxDb2Config {
    •         
    •         @Value("${spring.db2.datasource.xa-data-source-class-name}") String ds2XaDataSourceClassName;
    •         @Value("${spring.db2.datasource.xa-properties.user}") String ds2User;
    •         @Value("${spring.db2.datasource.xa-properties.password}") String ds2Password;
    •         @Value("${spring.db2.datasource.xa-properties.url}") String ds2Url;     // 이것 때문에 무지 삽질했다...
    •  
    •         private final Logger logger = LoggerFactory.getLogger(XxxDb2Config.class);
    •         
    •         public static final String DS2_DATASOURCE = "ds2DataSource";
    •                 
    •         /* ================> data source 2 start */
    •     @Bean(name=DS2_DATASOURCE)
    •     public DataSource dataSource() {
    •             AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
    •                 ds.setUniqueResourceName(DS2_DATASOURCE);
    •                 ds.setXaDataSourceClassName(ds2XaDataSourceClassName);
    •                 
    •                 Properties p = new Properties();
    •                 p.setProperty("user", ds2User);
    •                 p.setProperty("password", ds2Password);
    •                 p.setProperty("URL", ds2Url);   // 이것이 문제였다...젠장...
    •  
    •                 ds.setXaProperties (p);
    •                 
    •                 return ds;
    •     }
    •  
    •     @Bean(name="ds2SqlSessionFactory")
    •     public SqlSessionFactory sqlSessionFactory(@Qualifier(DS2_DATASOURCE) DataSource dataSource) throws Exception {
    •         final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
    •         sessionFactory.setDataSource(dataSource);
    •         return sessionFactory.getObject();
    •     }