두 개의 물리적으로 다른 Database에 트랜잭션을 적용하기 위해서 JTA를 사용해야 한다는 것을 듣고 그 중에 Atomikos를 사용하면 된다는 이야기에 구현을 해 보았다.

구글링을 해 보아도 이런말 저런말들이 많고 예전 기준으로 구현된 것도 많고 소스들이 너무나도 달라 뭘 어떻게 따라 해야 할지 막막했는데...

그리고 주로 MyBatis 기준으로 작업이 되어 있어서 답답한 감이 있었는데 JPA 기준으로 작업을 할 수 있도록 4일 정도 삽질 끝에 완성하여 백업 차원에서 기록으로 남긴다.

 

먼저 Legacy Database와 새롭게 사용하는 Database가 있다는 가정하에 아래와 같이 작업을 해야 한다.

먼저 사용한 의존성은 아래와 같다.

 

build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation('org.springframework.boot:spring-boot-starter-aop')
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation('org.springframework.data:spring-data-envers')
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'mysql:mysql-connector-java'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }

    // providedRuntime('org.springframework.boot:spring-boot-starter-tomcat')
    implementation('org.hibernate:hibernate-java8:5.1.0.Final')
    implementation('org.springframework.boot:spring-boot-starter-jta-atomikos')

    compileOnly('org.projectlombok:lombok:1.18.4')
    testCompileOnly('org.projectlombok:lombok:1.18.4')

    runtimeOnly 'mysql:mysql-connector-java'
}

 

그리고 사용하게 된 설정 파일은 아래와 같다.

 

application.yml

spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: goodchoice-user
    description: "User internal api for goodchoice"
  jackson:
    time-zone: "Asia/Seoul"
  host: host주소값
  # datasource 이하 값들은 Legacy Hikari datasource를 위한 값들
  datasource:
    lw:
      driverClassName: com.mysql.cj.jdbc.Driver
      jdbcUrl: jdbc:mysql://legacy-write-server:3306/databasename?autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
      username: id
      password: password
      maximumPoolSize: 30
      minimumIdle: 5
      poolName: write
      readOnly: false
    lr:
      driverClassName: com.mysql.cj.jdbc.Driver
      jdbcUrl: jdbc:mysql://legacy-read-server:3306/databasename?autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
      username: id
      password: password
      maximumPoolSize: 30
      minimumIdle: 5
      poolName: read
      readOnly: true
  # userbenefit 이하 값들은 신규 Hikari datasource를 위한 값들
  userbenefit:
    ubw:
      driverClassName: com.mysql.cj.jdbc.Driver
      jdbcUrl: jdbc:mysql://new-write-server:3306/databasename?autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
      username: id
      password: password
      maximumPoolSize: 30
      minimumIdle: 5
      poolName: write
      readOnly: false
    ubr:
      driverClassName: com.mysql.cj.jdbc.Driver
      jdbcUrl: jdbc:mysql://new-read-server:3306/databasename?autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
      username: id
      password: password
      maximumPoolSize: 30
      minimumIdle: 5
      poolName: read
      readOnly: true
  # jta 이하 값들은 jta atomikos에서 사용하기 위한 값들
  jta:
    enabled: true
    atomikos:
      datasource:
        lgyw:
          unique-resource-name: xaForLegacyDataSource
          xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource	# class로 사용되는 MysqlXADataSource 클래스는 mysql-connector-java의 버전에 따라 위치가 다르기 때문에 확인 필요
          xa-properties:
            user: id
            password: password
            url: jdbc:mysql://legacy-write-server:3306/databasename?autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
        ubgw:
          unique-resource-name: xaForUserbenefitDataSource
          xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
          xa-properties:
            user: id
            password: password
            url: jdbc:mysql://new-write-server:3306/databasename?autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
  jpa:
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        show_sql: fase
        use_sql_comments: true
        format_sql: true
        dialect: org.hibernate.dialect.MySQL55Dialect

 

먼저 Atomikos 사용을 위해서 필요한 UserTransation object와 TransactionManager를 담을 수 있는 클래스를 만들어 보자.

 

AtomikosJtaPlatform.java

package kr.co.within.goodchoice.user.jta.infrastructure.config;

import org.hibernate.engine.transaction.jta.platform.internal.AbstractJtaPlatform;

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

public class AtomikosJtaPlatform extends AbstractJtaPlatform {
    private static final long serialVersionUID = 1L;

    static TransactionManager transactionManager;
    static UserTransaction transaction;

    @Override
    protected TransactionManager locateTransactionManager() {
        return transactionManager;
    }

    @Override
    protected UserTransaction locateUserTransaction() {
        return transaction;
    }
}

 

위와 같이 담을 그릇이 준비 되었다면 해당 Object에 값을 담을 수 있는 클래스를 작성하자.

 

XaDataManagerConfig.java

package kr.co.within.goodchoice.user.jta.infrastructure.config;

import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
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;

@Slf4j
@Configuration
@ComponentScan
@EnableTransactionManagement
public class XaDataManagerConfig {
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(true);
        hibernateJpaVendorAdapter.setGenerateDdl(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);

        return hibernateJpaVendorAdapter;
    }

    @Bean(name = "userTransaction")
    public UserTransaction userTransaction() throws Throwable {
        log.info("========= userTransaction =========");
        UserTransactionImp userTransactionImp = new UserTransactionImp();
        userTransactionImp.setTransactionTimeout(10000);

        return userTransactionImp;
    }

    @Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
    public TransactionManager atomikosTransactionManager() throws Throwable {
        log.info("========= atomikosTransactionManager =========");
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        userTransactionManager.setForceShutdown(false);

        AtomikosJtaPlatform.transactionManager = userTransactionManager;

        return userTransactionManager;
    }

    @Bean(name = "multiTxManager")
    @DependsOn({"userTransaction", "atomikosTransactionManager"})
    public PlatformTransactionManager transactionManager() throws Throwable {
        log.info("========= transactionManager =========");
        UserTransaction userTransaction = userTransaction();

        AtomikosJtaPlatform.transaction = userTransaction;

        JtaTransactionManager manager = new JtaTransactionManager(userTransaction, atomikosTransactionManager());

        return manager;
    }
}

 

위 파일에서 중요한 부분은 AtomikosJtaPlatform.transactionManager와 AtomikosJtaPlatform.transaction에 값을 담는 부분이다.

그리고 multiTxManager bean에서 두 개의 bean (userTransation, atomikosTransactionManager)을 묶어서 JtaTransactionManager로 묶는 것이다.

 

이렇게까지 작업을 한 이후 실제 물리적으로 떨어져 있는 각각의 데이터베이스를 이용한 DataSource를 만들어 보자

 

먼저 Atomikos에서 사용하기 위한 Legacy DataSource 클래스를 작성해 보자.

파일 이름은 XaForLegacyDataSourceConfig 로 작성했다.

사용된 XA 이름은 아래 참고 링크에서 볼 수 있듯이 분산트랜잭션 처리를 위한 표준 스팩 이름이기 때문에 사용하였다.

 

https://d2.naver.com/helloworld/5812258

불러오는 중입니다...

 

XaForLegacyDataSourceConfig.java

package kr.co.within.goodchoice.user.jta.infrastructure.config;

import com.atomikos.jdbc.AtomikosDataSourceBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.util.Properties;

@Slf4j
@Configuration
@DependsOn("multiTxManager")
@EnableTransactionManagement
@EnableJpaAuditing
@EnableJpaRepositories(
        basePackages = {"kr.co.within.goodchoice.user.jta.domain.legacy"}
        , entityManagerFactoryRef = "xaEntityManagerFactory"
        , transactionManagerRef = "multiTxManager"
)
@EntityScan("kr.co.within.goodchoice.user.jta.domain.legacy")
public class XaForLegacyDataSourceConfig {
    @Value("${spring.jta.atomikos.datasource.lgyw.unique-resource-name}")
    private String uniqueResourceName;

    @Value("${spring.jta.atomikos.datasource.lgyw.xa-data-source-class-name}")
    private String dataSourceClassName;

    @Value("${spring.jta.atomikos.datasource.lgyw.xa-properties.user}")
    private String user;

    @Value("${spring.jta.atomikos.datasource.lgyw.xa-properties.password}")
    private String password;

    @Value("${spring.jta.atomikos.datasource.lgyw.xa-properties.url}")
    private String url;

    @Autowired
    private JpaVendorAdapter jpaVendorAdapter;

    @Bean(name = "xaForLegacyDataSource")
    public DataSource xaForLegacyDataSource() {
        log.info("==================== xaForLegacyDataSource");
        Properties properties = new Properties();
        properties.setProperty("url", url);
        properties.setProperty("user", user);
        properties.setProperty("password", password);

        AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
        dataSource.setUniqueResourceName(uniqueResourceName);
        dataSource.setXaDataSourceClassName(dataSourceClassName);
        dataSource.setXaProperties(properties);

        return dataSource;
    }

    @Bean(name = "xaEntityManagerFactory")
    @DependsOn("multiTxManager")
    public LocalContainerEntityManagerFactoryBean xaEntityManagerFactory() {
        log.info("==================== legacyEntityManagerFactory");
        Properties properties = new Properties();
        properties.put("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName());
        properties.put("javax.persistence.transactionType", "JTA");

        LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
        entityManager.setDataSource(xaForLegacyDataSource());
        entityManager.setJpaVendorAdapter(jpaVendorAdapter);
        entityManager.setPackagesToScan("kr.co.within.goodchoice.user.jta.domain.legacy");
        entityManager.setPersistenceUnitName("legacy_write_unit");
        entityManager.setJpaProperties(properties);

        return entityManager;
    }
}

 

필요한 접속 정보를 셋팅하고 해당 값으로 Datasource를 만든 다음 entityManagerFactory를 생성 한다.

접속 정보를 @Value로 가지고 와도 되지만 아래처럼 해도 된다. (해보지는 않았지만 될 듯?)

DataSource를 만드는 메소드 위에 아래와 같은 어노테이션을 붙이면 된다.

 

@ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.lgyw.xa")

 

그리고 나서 중요한 것은 위에 XaDataManagerConfig.java에서 만들어 둔 multiTxManager bean에 대한 의존성을 적용해 줘야 한다.

클래스 상단과 entityManagerFactory 상단에 @DependsOn("multiTxManager") 를 넣으면 된다.

 

이 처럼 legacy쪽 Datasource 작업이 끝났다면 신규 Datasource쪽을 작업해 주면 된다. 소스는 비슷하다.

 

XaForUserbenefitDataSourceConfig.java

package kr.co.within.goodchoice.user.jta.infrastructure.config;

import com.atomikos.jdbc.AtomikosDataSourceBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.util.Properties;

@Slf4j
@Primary
@Configuration
@DependsOn("multiTxManager")
@EnableTransactionManagement
@EnableJpaAuditing
@EnableJpaRepositories(
        basePackages = {"kr.co.within.goodchoice.user.jta.domain.userbenefit"}
        , entityManagerFactoryRef = "userbenefitEntityManagerFactory"
        , transactionManagerRef = "multiTxManager"
)
@EntityScan("kr.co.within.goodchoice.user.jta.domain.userbenefit")
public class XaForUserbenefitDataSourceConfig {
    @Value("${spring.jta.atomikos.datasource.ubgw.unique-resource-name}")
    private String uniqueResourceName;

    @Value("${spring.jta.atomikos.datasource.ubgw.xa-data-source-class-name}")
    private String dataSourceClassName;

    @Value("${spring.jta.atomikos.datasource.ubgw.xa-properties.user}")
    private String user;

    @Value("${spring.jta.atomikos.datasource.ubgw.xa-properties.password}")
    private String password;

    @Value("${spring.jta.atomikos.datasource.ubgw.xa-properties.url}")
    private String url;

    @Autowired
    private JpaVendorAdapter jpaVendorAdapter;

    @Primary
    @Bean(name = "xaForUserbenefitDataSource")
    public DataSource xaForUserbenefitDataSource() {
        log.info("==================== xaForUserbenefitDataSource");
        Properties properties = new Properties();
        properties.setProperty("url", url);
        properties.setProperty("user", user);
        properties.setProperty("password", password);

        AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
        dataSource.setUniqueResourceName(uniqueResourceName);
        dataSource.setXaDataSourceClassName(dataSourceClassName);
        dataSource.setXaProperties(properties);

        return dataSource;
    }

    @Primary
    @Bean(name = "userbenefitEntityManagerFactory")
    @DependsOn("multiTxManager")
    public LocalContainerEntityManagerFactoryBean userbenefitEntityManagerFactory() {
        log.info("==================== userbenefitEntityManagerFactory");
        Properties properties = new Properties();
        properties.put("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName());
        properties.put("javax.persistence.transactionType", "JTA");

        LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
        entityManager.setDataSource(xaForUserbenefitDataSource());
        entityManager.setJpaVendorAdapter(jpaVendorAdapter);
        entityManager.setPackagesToScan("kr.co.within.goodchoice.user.jta.domain.userbenefit");
        entityManager.setPersistenceUnitName("write_unit");
        entityManager.setJpaProperties(properties);

        return entityManager;
    }
}

 

legacy쪽 소스와 다른 점은 클래스와 각 메소드 위에 @Primary 를 사용했다는 점이다.

해당 어노테이션을 붙이지 않을 경우 오류로그에서 기대하는 datasource는 1개인데 그 이상이 들어왔다와 비슷한 오류를 보게 될 것이다.

 

이렇게까지 해 놓고 실제 사용을 해 보자.

필요한 것은 legacy에서 사용할 entity, repository, service 그리고 신규에서 사용할 entity, repository, service 이다.

또한 두 개의 서비스를 묶어 줄 또 하나의 상위 개념 service가 필요하다.

각각의 entity, repository, service는 간단하게 save만 하는 것이 필요하므로 생략하고 두 개를 묶어 줄 service의 소스 코드는 아래와 같다.

 

ApiService.java

package kr.co.within.goodchoice.user.jta.domain;

import kr.co.within.goodchoice.user.jta.domain.legacy.entity.LegacyUser;
import kr.co.within.goodchoice.user.jta.domain.legacy.service.LegacyUserService;
//import kr.co.within.goodchoice.user.jta.domain.userbenefit.entity.UserAddInfo;
//import kr.co.within.goodchoice.user.jta.domain.userbenefit.service.UserAddInfoService;
import kr.co.within.goodchoice.user.jta.domain.userbenefit.entity.UserAddInfo;
import kr.co.within.goodchoice.user.jta.domain.userbenefit.service.UserAddInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

@Slf4j
@Service
public class ApiService {
    @Autowired
    private LegacyUserService legacyUserService;

    @Autowired
    private UserAddInfoService userAddInfoService;

    @Transactional(propagation = Propagation.REQUIRED, transactionManager = "multiTxManager")
    public void addUser() {
        // legacy DB에서 사용되는 entity객체
        LegacyUser legacyUser = LegacyUser.builder()....build();

        // 신규에서 사용될 entity 객체
        UserAddInfo userAddInfo = UserAddInfo.builder()....build();

        legacyUserService.addUser(legacyUser);
        userAddInfoService.addUserInfo(userAddInfo);
    }
}

 

atomikos를 통해 만든 multiTxManager를 사용한다고 메소드 상단에 명시하고 각가 다른 두 개의 서비스를 호출해 보자.

userAddInfoService.addUserInfo 메소드에서는 다음과 같이 작업을 한다.

 

UserAddInfoService.java

package kr.co.within.goodchoice.user.jta.domain.userbenefit.service;

import kr.co.within.goodchoice.user.jta.domain.userbenefit.entity.UserAddInfo;
import kr.co.within.goodchoice.user.jta.domain.userbenefit.repository.UserAddInfoRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class UserAddInfoService {
    @Autowired
    private UserAddInfoRepository userAddInfoRepository;

    public void addUserInfo(UserAddInfo userAddInfo) {
        userAddInfoRepository.save(userAddInfo);
        throw new RuntimeException("add UserInfo fail...");
    }
}

 

일부러 Exception을 일으켜 보면 legacy와 신규 DB 모두에 값이 안들어 가게 될 것이고 exception 부분을 지우고 테스트 하면 잘 들어가게 될 것이다.

개인적으로 테스트 시 이상 없이 동작함을 볼 수 있었다.

 

참고로 테스트를 unitTest를 사용하지 않은 이유는 unitTest를 사용하게 될 경우 자동으로 rollback이 되기 때문이다.

 

이와는 별도로 Hikari Pool을 이용하여 Datasource와 TransactionManager를 만들어 두고 ApiService.java 파일에서 해당 transactionManager를 명시하게 된다면 원하는대로 동작하지 않고 두 Database 모두에 커밋 되거나 하나만 커밋 되는 경우를 볼 수 있다.

 

Hikari Pool을 이용하여 다수의 Datasource를 사요하는 예제는 아래의 포스트를 참고하기 바란다.

2019/08/13 - [Java/Spring] - 멀티 DataSource 접속 방법 정리

 

이상으로 삽질을 마치며 아래는 구글링 하면서 알게 된 참조 사이트 들이다. 훨씬 더 많지만 기억이 안나기 때문에 아래 사이트만 남긴다.

 

https://d2.naver.com/helloworld/5812258

https://bigzero37.tistory.com/65

 

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

Atomikos를 이용한 이기종 DB 트랜잭션(Springboot + Mybatis) - 2. Application 환경구성 및 샘플코드 Springboot 및 Mybatis 를 위한 Config 설정 application.properties 정의 spring.jta.enabled=true # DATAS..

bigzero37.tistory.com

https://supawer0728.github.io/2018/03/22/spring-multi-transaction/

 

(Spring)다중 DataSource 처리

서론Spring Application을 만들면서 여러 DataSource와 transaction이 존재하고 하나의 transaction 내에 commit과 rollback이 잘 동작하도록 하려면 어떻게 설정해야 할까? 실제로 구현을 해본 적은 없지만 세 가지 방법이 머릿속에 떠올랐다. @Transactional의 propagation을 이용 spring-

supawer0728.github.io

 




WRITTEN BY
체리필터
프로그램 그리고 인생...

트랙백  0 , 댓글  0개가 달렸습니다.
secret


한 개의 프로젝트에서 하나의 Database에만 접속하는 경우가 대부분이지만, 2개 이상의 데이터 베이스에 접속하는 경우도 발생하게 된다.

이럴 경우 어떻게 셋팅을 해야 하는지 정리한다.

 

application.yml 설정파일

spring:
    profiles:
        active: local
    application:
        name: usercms

application-local.yml

server:
    port: 9090
spring:
    profiles: local
    domain: localhost
    datasource-a-write:
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://a-db-url:3306/a?autoReconnect=true&useSSL=false
        username: id
        password: password
    datasource-a-read:
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://a-db-url:3306/a?autoReconnect=true&useSSL=false
        username: id
        password: password
    datasource-b-write:
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://b-db-url:3306/b?autoReconnect=true&useSSL=false
        username: id
        password: password
    datasource-b-read:
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://b-db-url:3306/b?autoReconnect=true&useSSL=false
        username: id
        password: password

 

Database의 read, write 구분을 위한 설정

import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.support.TransactionSynchronizationManager;

@Slf4j
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        String dataSourceType = TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? "read" : "write";
        return dataSourceType;
    }
}

 

a 서버에 접속하기 위한 DataSource

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Configuration
public class ADataSourceConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource-a-read")
    public DataSource aReadDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource-a-write")
    public DataSource aWriteDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    public DataSource aRoutingDataSource(@Qualifier("aWriteDataSource") DataSource writeDataSource, @Qualifier("aReadDataSource") DataSource readDataSource) {
        ReplicationRoutingDataSource aRoutingDataSource = new ReplicationRoutingDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<Object, Object>();
        dataSourceMap.put("write", writeDataSource);
        dataSourceMap.put("read", readDataSource);
        aRoutingDataSource.setTargetDataSources(dataSourceMap);
        aRoutingDataSource.setDefaultTargetDataSource(aReadDataSource());

        return aRoutingDataSource;
    }

    @Primary
    @Bean
    public DataSource aDataSource(@Qualifier("aRoutingDataSource") DataSource routingDataSource) {
        log.debug("#### DATA SOURCE");
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }
}

 

a entity에 a DataSource를 맵핑하기 위한 소스. a를 메인으로 사용하기 위해 @Primary 어노테이션 사용

import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
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.persistence.EntityManagerFactory;
import javax.sql.DataSource;

@Configuration
//@ComponentScan
@EnableTransactionManagement
@EnableJpaRepositories(
        basePackages = {
                "a DataSource를 이용하는 도메인의 entity가 위치한 패키지"
        },
        entityManagerFactoryRef = "aEntityManagerFactory",
        transactionManagerRef = "aTransactionManager"
)
public class ADataManagerConfig {
    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean aEntityManagerFactory(@Qualifier("aDataSource") DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean();
        emfb.setDataSource(dataSource);
        emfb.setPersistenceProvider(new HibernatePersistenceProvider());
        emfb.setPersistenceUnitName("aEntityManager");
        emfb.setPackagesToScan(
                "a DataSource를 이용하는 도메인의 entity가 위치한 패키지"
        );
        HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
        jpaVendorAdapter.setShowSql(true);
        jpaVendorAdapter.setGenerateDdl(false);
//        //properties.setProperty(“hibernate.hbm2ddl.auto”, “none”);
//        Properties properties = new Properties();
//        properties.setProperty("show_sql", "true");
//        emfb.setJpaProperties(properties);
        emfb.setJpaVendorAdapter(jpaVendorAdapter);

        return emfb;
    }

    @Primary
    @Bean
    public PlatformTransactionManager aTransactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }

    @Bean
    public PersistenceExceptionTranslationPostProcessor aExceptionTranslationPostProcessor() {
        return new PersistenceExceptionTranslationPostProcessor();
    }
}

 

b 서버에 접속하기 위한 DataSource

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Configuration
public class BDataSourceConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource-b-read")
    public DataSource bReadDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource-b-write")
    public DataSource bWriteDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    public DataSource bRoutingDataSource(@Qualifier("bWriteDataSource") DataSource writeDataSource, @Qualifier("bReadDataSource") DataSource readDataSource) {
        ReplicationRoutingDataSource bRoutingDataSource = new ReplicationRoutingDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<Object, Object>();
        dataSourceMap.put("write", writeDataSource);
        dataSourceMap.put("read", readDataSource);
        bRoutingDataSource.setTargetDataSources(dataSourceMap);
        bRoutingDataSource.setDefaultTargetDataSource(bReadDataSource());

        return bRoutingDataSource;
    }

    @Primary
    @Bean
    public DataSource bDataSource(@Qualifier("bRoutingDataSource") DataSource routingDataSource) {
        log.debug("#### DATA SOURCE");
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }
}

 

b entity에 b DataSource를 맵핑하기 위한 소스

import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
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.persistence.EntityManagerFactory;
import javax.sql.DataSource;

@Configuration
//@ComponentScan
@EnableTransactionManagement
@EnableJpaRepositories(
        basePackages = {
                "b DataSource를 이용하는 도메인의 entity가 위치한 패키지"
        },
        entityManagerFactoryRef = "bEntityManagerFactory",
        transactionManagerRef = "bTransactionManager"
)
public class BDataManagerConfig {
    @Bean
    public LocalContainerEntityManagerFactoryBean bEntityManagerFactory(@Qualifier("bDataSource") DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean();
        emfb.setDataSource(dataSource);
        emfb.setPersistenceProvider(new HibernatePersistenceProvider());
        emfb.setPersistenceUnitName("bEntityManager");
        emfb.setPackagesToScan(
                "b DataSource를 이용하는 도메인의 entity가 위치한 패키지"
        );
        HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
        jpaVendorAdapter.setShowSql(true);
        jpaVendorAdapter.setGenerateDdl(false);
//        //properties.setProperty(“hibernate.hbm2ddl.auto”, “none”);
//        Properties properties = new Properties();
//        properties.setProperty("show_sql", "true");
//        emfb.setJpaProperties(properties);
        emfb.setJpaVendorAdapter(jpaVendorAdapter);

        return emfb;
    }

    @Bean
    public PlatformTransactionManager bTransactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }

    @Bean
    public PersistenceExceptionTranslationPostProcessor bExceptionTranslationPostProcessor() {
        return new PersistenceExceptionTranslationPostProcessor();
    }
}

 

이와 같이 하게 되면 2개의 별도 DB에 접속할 수 있다.

다만 QueryDSL을 사용하게 될 경우 기존에는 QuerydslRepositorySupport을 상속받아 사용했는데, 이렇게 되면 2개 중 어떤 것을 사용해야 할지 몰라 에러를 내게 된다.

따라서 다음과 같이 QuerydslRepositorySupport을 상속받아 특정 메소드를 OverRide 한 클래스를 만들어야 한다.

그리고 QueryDSL을 사용하는 곳에서는 새롭게 상속받아 작성한 클래스를 상속받아 사용하면 된다.

 

a QuerydslRepositorySupport

import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Repository
public abstract class AQueryDslRepositorySupport extends QuerydslRepositorySupport {
    public AQueryDslRepositorySupport(Class<?> domainClass) {
        super(domainClass);
    }

    @Override
    @PersistenceContext(unitName = "aEntityManager")
    public void setEntityManager(EntityManager entityManager) {
        super.setEntityManager(entityManager);
    }
}

 

b QuerydslRepositorySupport

import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Repository
public abstract class BQueryDslRepositorySupport extends QuerydslRepositorySupport {
    public BQueryDslRepositorySupport(Class<?> domainClass) {
        super(domainClass);
    }

    @Override
    @PersistenceContext(unitName = "bEntityManager")
    public void setEntityManager(EntityManager entityManager) {
        super.setEntityManager(entityManager);
    }
}

 

각 entityManger에 맞게 QueryDSL을 사용하게 되는 경우...

import kr.co.within.cms.user.api.infrastructure.config.AQueryDslRepositorySupport;

public class AQueryRepositoryImpl extends AQueryDslRepositorySupport implements AQueryRepositoryCustom {
    public AQueryRepositoryImpl() {
        super(A.class);
    }
}
import kr.co.within.cms.user.api.infrastructure.config.BQueryDslRepositorySupport;

public class BQueryRepositoryImpl extends BQueryDslRepositorySupport implements BQueryRepositoryCustom {
    public BQueryRepositoryImpl() {
        super(B.class);
    }
}

 

기억이 짧아 자꾸 까먹는 나를 위하여 기록한다.

 

참고. 멀티 DataSource에서 트랜잭션을 처리하기 위해서는 아래 포스팅 참조.

https://supawer0728.github.io/2018/03/22/spring-multi-transaction/

 

(Spring)다중 DataSource 처리

서론Spring Application을 만들면서 여러 DataSource와 transaction이 존재하고 하나의 transaction 내에 commit과 rollback이 잘 동작하도록 하려면 어떻게 설정해야 할까? 실제로 구현을 해본 적은 없지만 세 가지 방법이 머릿속에 떠올랐다. @Transactional의 propagation을 이용 spring-

supawer0728.github.io

 

 




WRITTEN BY
체리필터
프로그램 그리고 인생...

트랙백  0 , 댓글  0개가 달렸습니다.
secret


JPA를 사용하면서 QueryDSL을 셋팅하고 사용하는 부분에 있어서 매번 헷깔려 정리한다.

 

QueryDSL을 사용하기 위해서 build.gradle 파일에 아래의 내용을 추가 해 준다.

plugins {
	...
    
    id 'idea'
    id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'
    
    ...
}

dependencies {
	...
    
    implementation 'com.querydsl:querydsl-apt:4.1.4'
    implementation 'com.querydsl:querydsl-jpa:4.1.4'
    
    ...
}

ext {
    querydslSrcDir = 'src/main/generated'
    queryDslVersion = '4.1.4'
}

configurations {
    querydsl.extendsFrom compileClasspath
}

querydsl {
    library = "com.querydsl:querydsl-apt"
    querydslSourcesDir = 'src/main/generated'
    jpa = true
    querydslDefault = true
}

sourceSets {
    main {
        java {
            srcDirs += file(querydslSrcDir)
        }
    }
}

idea {
    module {
        generatedSourceDirs += file(querydslSrcDir)
    }
}

 

Entity 작성

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@EntityListeners(value = { AuditingEntityListener.class })
@Table(name = "user")
@Getter
@Setter
public class YeogiUser {
    @Id
    @Column(name="uno")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;
    
    ....
    
    @UpdateTimestamp
    @Column(name = "uedit")
    private LocalDateTime updatedAt;

    @CreationTimestamp
    @Column(name="ureg", updatable = false)
    private LocalDateTime createdAt;
    
    ....
    
    @QueryProjection
    public YeogiUser(long userId, LocalDateTime updatedAt, LocalDateTime createdAt) {
        this.userId = userId;
        this.updatedAt = updatedAt;
        this.createdAt = createdAt;
    }
}

 

기본 JPA Interface 작성

public interface YeogiUserQueryRepository extends JpaRepository<YeogiUser, Long>, YeogiUserQueryRepositoryCustom {
    ...
}

 

Custom으로 QueryDSL을 사용할 interface 작성

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface YeogiUserQueryRepositoryCustom {
    Page<YeogiUser> findByUserParamForIpcc(Pageable pageable, UserParamForIpcc userParamForIpcc);
}

 

Custom interface 구현 클래스 작성

import com.querydsl.core.QueryResults;
import com.querydsl.core.types.ConstructorExpression;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import kr.co.within.cms.user.api.application.params.UserParamForIpcc;
import kr.co.within.cms.user.api.domain.yeogi.user.entity.QYeogiUser;
import kr.co.within.cms.user.api.domain.yeogi.user.entity.YeogiUser;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;

public class YeogiUserQueryRepositoryImpl extends QuerydslRepositorySupport implements YeogiUserQueryRepositoryCustom {
    public YeogiUserQueryRepositoryImpl() {
        super(YeogiUser.class);
    }

    @Override
    public Page<YeogiUser> findByUserParamForIpcc(Pageable pageable, UserParamForIpcc userParamForIpcc) {
        JPAQueryFactory query = new JPAQueryFactory(this.getEntityManager());
        QYeogiUser yegiUser = QYeogiUser.yeogiUser;

        JPAQuery<YeogiUser> jpaQuery = query
                .select(getYeogiUserProjection())
                .from(yegiUser);

        if (CollectionUtils.isEmpty(userParamForIpcc.getUserIdList()) == false) {
            jpaQuery = jpaQuery.where(yegiUser.userId.in(userParamForIpcc.getUserIdList()));
        }

        if (StringUtils.isNotEmpty(userParamForIpcc.getLoginId())) {
            jpaQuery = jpaQuery.where(yegiUser.loginId.eq(userParamForIpcc.getLoginId()));
        }

        if (StringUtils.isNotEmpty(userParamForIpcc.getNickname())) {
            jpaQuery = jpaQuery.where(yegiUser.nickname.eq(userParamForIpcc.getNickname()));
        }

        if (StringUtils.isNotEmpty(userParamForIpcc.getPhone())) {
            jpaQuery = jpaQuery.where(yegiUser.phone.eq(userParamForIpcc.getPhone()));
        }

        QueryResults<YeogiUser> list = jpaQuery
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(new OrderSpecifier(Order.ASC, yegiUser.userId))
                .fetchResults();

        return new PageImpl<>(list.getResults(), pageable, list.getTotal());
    }

    private ConstructorExpression<YeogiUser> getYeogiUserProjection() {
        QYeogiUser yegiUser = QYeogiUser.yeogiUser;

        return Projections.constructor(YeogiUser.class,
                yegiUser.userId, yegiUser.userStatus, yegiUser.userType, yegiUser.ano, yegiUser.loginId, yegiUser.loginPassword,
                yegiUser.nickname, yegiUser.name, yegiUser.facebookId, yegiUser.updatedAt, yegiUser.createdAt,
                yegiUser.deviceId, yegiUser.myRecommendationCode, yegiUser.friendRecommendationCode, yegiUser.recommededCount,
                yegiUser.snsAgree, yegiUser.uaname, yegiUser.group, yegiUser.alarmStatus, yegiUser.latestLodging,
                yegiUser.alarmAgreeDate, yegiUser.phone, yegiUser.lastLoginAt);
    }
}

 

사용

    @Autowired
    private YeogiUserQueryRepository yeogiUserQueryRepository;

    public Page<YeogiUser> getUserList(Pageable pageable, UserParamForIpcc userParamForIpcc) {
        return yeogiUserQueryRepository.findByUserParamForIpcc(pageable, userParamForIpcc);
    }

 

자세한 설명은 생략한다.

 




WRITTEN BY
체리필터
프로그램 그리고 인생...

트랙백  0 , 댓글  0개가 달렸습니다.
secret


JPA, QueryDsl 쓰기 참 힘들다.

그래도 하나 하나 어렵게 해 나가고 있는데 오늘은 또 Group By한 Count 개수를 반환하는 것을 만든 삽질 내용을 올린다.

 

리파지토리 단 소스는 아래와 같다.

@Override
public QueryResults<Tuple> getBusinessTotalCount() {
    QUser user = QUser.user;

    return from(user).where(user.userType.eq(UserType.B2B)).groupBy(user.platform).select(user.platform, user.platform.count()).fetchResults();
}

 

 

그리고 가지고 온 내용을 가지고 맵으로 이쁘게 정렬해서 반환해 주면 된다.

public Map<String,Integer> getBusinessTotalCount() {
	QueryResults<Tuple> queryResults = userQueryRepository.getBusinessTotalCount();

	Map<String,Integer> returnMap = new HashMap<>();
	List<Tuple> list = queryResults.getResults();
	for (Tuple tuple : list) {
		returnMap.put(tuple.get(0, String.class), tuple.get(1, Integer.class));
	}

	return returnMap;
}

 

 

코드만 놓고 보자면 별거 아닌데, 모르는 상태에서 찾아서 할려니 진도가 더디다. ㅠㅠ

 

 




WRITTEN BY
체리필터
프로그램 그리고 인생...

트랙백  0 , 댓글  0개가 달렸습니다.
secret