ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring boot Database 다중 연결 ( feat.Jooq )
    Spring/Spring Boot 2019. 10. 28. 09:12
    반응형

    이전에 블로그에 spring boot 로 여러개의 데이터베이스를 연결하는걸 작성한적이 있다. 어떤 서버든 개인적으로는 하나의 데이터베이스만 접근해서 사용하는게 좋다고 생각하지만 ( MSA 스럽지 못하다! ) 어쩔수 없이 여러 데이터베이스의 정보를 가져와야 하는경우가 생긴다. 가령 이곳저곳 데이터를 수집하는 경우라면.... 

     API 로 제공 받아서 연동하는게 가장 best 한 case 가 되겠지만 지금 상황(?) 상 api 를 제공 받지 못하는 상황이라면 직접 데이터베이스에 접근해야만 하는 상황이 생겼다. 

     이전 글( https://ellune.tistory.com/4?category=769021) 에서 여러개를 설정하는 방법을 찾았지만 문제가 생겼다. 

     

     1. 이전글에서는 mybatis 를 사용 하고 있다. 하지만 현재는 Jooq 을 쓰고 있다. 

     2. 이전경우는 트랜젝션이 중요치 않았다. 하지만 지금은 두개이상의 데이터베이스를 연결함에 중요하게 되버렸다. 

     

    zepinos 님의 도움을 받아서 다른 방법을 찾게 되었고 나름 성공(?) 적인 결과를 낸거 같다. 항상 그렇듯 이런 설정 문제는 모를땐 어렵지만 알고나면 참 별것도 아닌경우가 많은거 같다. 

     

     일단, 두가지 기능을 사용해야 한다. jta 와 xa 란것을 알아야 한다. 

     

    JTA : java Transaction API 

    XA : 하나의 golobal transction으로 여러개의 데이터베이스에 접근하기 위한 x/open 그룹 표준 

     

    참 다행인건 JTA 를 기본적인 오픈소스로 제공을 한다. atomikos 를 사용 하면 쉽게 사용이 가능하다. 친절한 IBM 에서 서버의 일부분으로 구현체를 제공하고 있다. https://www.oracle.com/technetwork/java/javaee/jta/index.html이 주소에 가보면 해당 스펙을 확인이 가능하다. 

     기본적으로 이전글에도 확인했었지만 spring boot 는 기존 spring framework 보다 여러개의 데이터베이스를 연결하기에는 불친절하다. 그래서 작업을 해줘야할것이 제법 많다. 

     

    일단 maven 설정부터 한다. spring 을 쓰면서 많이도 지원한다고 느낀적이 많은데 이번에도 역시 atomikos 를 지원을 한다. 그외에는 jooq 을 사용하기 위한 설정을 추가 했다. 

     <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-jooq</artifactId>
            </dependency>
    <dependency>
                <groupId>org.jooq</groupId>
                <artifactId>jooq-meta</artifactId>
            </dependency>
            <dependency>
                <groupId>org.jooq</groupId>
                <artifactId>jooq-codegen</artifactId>
    </dependency>
    
    <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-jta-atomikos</artifactId>
    </dependency>

    이 설정이 다됬다면 이제 진짜 설정을 본격적으로 해볼것이다. 설정을 하기 위해서 yml 을 사용 하지만 항상 그렇듯 java config 를 사용할 예정이다. 

     먼저 설정해야할것은 메인 케넥션에 대한 설정이다. 정리를 해놓자면 메인 설정은 기본으로 해야만한다. 안그러면 spring boot 에서 어느 커넥션이 우선순위가 높은지 모르겠다며 구동시 에러를 내뱉게 된다. 

    import org.jooq.DSLContext;
    import org.jooq.SQLDialect;
    import org.jooq.impl.DefaultConfiguration;
    import org.jooq.impl.DefaultDSLContext;
    import org.jooq.impl.DefaultExecuteListenerProvider;
    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.DependsOn;
    import org.springframework.context.annotation.Primary;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    
    import javax.sql.DataSource;
    
    @Configuration
    @EnableTransactionManagement
    public class DatabaseConfig {
    
        @Bean(name="originalDataSource")
        @Primary
        @ConfigurationProperties(prefix = "spring.original.datasource")
        public DataSource articleDataSource() {
            return DataSourceBuilder.create().build();
        }
        @Primary
        @Bean(name = "originalTxManager")
        public PlatformTransactionManager transactionManager (@Qualifier("firstDataSource") DataSource dataSource){
            return  new DataSourceTransactionManager(dataSource);
        }
    
    
        @Bean(name ="originalDsl")
        @DependsOn({"originalDataSource"})
        DSLContext dslContext() throws Exception {
            DefaultConfiguration jooqConfiguration = new DefaultConfiguration();
            jooqConfiguration.set( SQLDialect.MYSQL );
            jooqConfiguration.set( articleDataSource()  );
            jooqConfiguration.set( new DefaultExecuteListenerProvider(new JOOQToSpringExceptionTransformer()) );
            DSLContext dslContext = new DefaultDSLContext(jooqConfiguration);
            return dslContext;
        }
    
    }
    

     아주 중요한 설정이다. 다른설정을 다해도 이설정이 없다면 제대로 동작이 불가능하다. 간략하게 설명하면 articleDataSource() 에서 datasource 를 생성하고 transactionManager() 에서 그에 맞는 트랙젝션 설정을 한다. 그리고 jooq 을 사용하기 위한 핵심인데 dslContext() 에서 jooq 을 사용하기 위한 설정을 하는것이다. 이 구조에 대해서 개념을 너무 어렵게 생각해서 헤매고있었는데 zepinos 님 덕분에 좀 수월하게 정리가 된거 같다. 

     원리는 간단하다! 트랜젝션 설정까지는 철저히 spring boot 영역이다. 그리고 그걸 이용하는건 mybatis, jpa, jooq 각자의 설정에 따라 알아서 가져가는걸로 이해하면된다. 그렇기 때문에 dslContext() 이부분을 mybatis 라면 sessionFactory 설정을 해주면 되고 JPA 면 JPAsession 설정으로 교체만 해주면 되는것이다. 참 간단하다! 

     

    추가 적으로 jooq 을 사용하게 될경우 jooq 에서 발생하는 Exception 을 catch 하여 처리할 필요가 있다. 그럴때 보통 Exception 처리를 위한 설정을 해주게 되는데 위에 설정에 보면 new JOOQToSpringExceptionTransformer() 이란것을 사용하고 있다. 이부분도 정의 해주도록 하자.

    
    import org.jooq.ExecuteContext;
    import org.jooq.SQLDialect;
    import org.jooq.impl.DefaultExecuteListener;
    import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
    import org.springframework.jdbc.support.SQLExceptionTranslator;
    import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator;
    
    public class JOOQToSpringExceptionTransformer extends DefaultExecuteListener {
    
        @Override
        public void exception(ExecuteContext ctx) {
            SQLDialect dialect = ctx.configuration().dialect();
            SQLExceptionTranslator translator = (dialect != null)
                    ? new SQLErrorCodeSQLExceptionTranslator(dialect.name())
                    : new SQLStateSQLExceptionTranslator();
    
            ctx.exception(translator.translate("jOOQ", ctx.sql(), ctx.sqlException()));
        }
    
    }

     

    이제부터 각 데이터베이스에 대한 각각의 설정을 해주려고 한다. 먼저 yml 설정이 필요하다 각 디비의 접속정보를 세팅해주는데 기존의 jdbc 를 설정하는 방식과는 많이 다르다. 

     

    spring:
      jta:
        enabled: true
    
    original :
          datasource:
              jdbc-url: jdbc:mysql://xxxxxxxxxxx.com:3306
              password: xxxxx
              username: xxxxxxx
              driverClassName: com.mysql.cj.jdbc.Driver
    
      first :
        datasource:
          xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
          xa-properties:
            server-name: xxxxxxxxxxxxx.com
            database-name: xxxxx
            password: xxxxxxx
            user: xxxxxx
            port-number: 3306
      second:
          datasource:
            xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
            xa-properties:
              server-name: localhost
              database-name: test
              password: test
              user: test
              port-number: 3306
    
    
    
      jooq:
        sql-dialect: mysql

    메인 커넥션을 제외하고 XA 를 사용하기 때문에 설정 자체도 XA 기반으로 해주어야 한다. 여기서 중요한건 메인 디비 정보도 xa 로 한번더 설정해준다는것이다. 왜냐하면 하나의 트랜잭션으로 묶어서 사용하기때문인거 같다. 그리고 jta 설정을 true 로 명시하여 spring boot 가 인지하도록 해준다. 각각에 들어가는 값들을 보면 기존과 다른것이 없기 때문에 그게 어려울것이 없다. 

    기본 설정이 끝났다면 아래와 같이  각 디비에 대한 XA 설정을 해주면된다. 

    import org.jooq.DSLContext;
    import org.jooq.SQLDialect;
    import org.jooq.impl.DefaultConfiguration;
    import org.jooq.impl.DefaultDSLContext;
    import org.jooq.impl.DefaultExecuteListenerProvider;
    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.context.annotation.DependsOn;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import javax.sql.DataSource;
    import java.util.Properties;
    
    @Configuration
    @EnableTransactionManagement
    public class MainDbConfig {
         
        public static final String DS1_DATASOURCE = "ds0DataSource";
        
        @Value("${spring.first.datasource.xa-data-source-class-name}") String ds1XaDataSourceClassName;
        @Value("${spring.first.datasource.xa-properties.user}") String ds1User;
        @Value("${spring.first.datasource.xa-properties.password}") String ds1Password;
        @Value("${spring.first.datasource.xa-properties.server-name}") String ds1ServerName;
        @Value("${spring.first.datasource.xa-properties.port-number}") String ds1PortNumber;
        @Value("${spring.first.datasource.xa-properties.database-name}") String ds1DatabaseName;
    
        
    
        @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="firstDsl")
        @DependsOn({DS1_DATASOURCE})
        DSLContext dslContext() throws Exception {
            DefaultConfiguration jooqConfiguration = new DefaultConfiguration();
            jooqConfiguration.set( SQLDialect.MYSQL );
            jooqConfiguration.set( dataSource()  );
            jooqConfiguration.set( new DefaultExecuteListenerProvider(new JOOQToSpringExceptionTransformer()) );
            DSLContext dslContext = new DefaultDSLContext(jooqConfiguration);
            return dslContext;
        }
    
    }
    

     

    import org.jooq.DSLContext;
    import org.jooq.SQLDialect;
    import org.jooq.impl.DefaultConfiguration;
    import org.jooq.impl.DefaultDSLContext;
    import org.jooq.impl.DefaultExecuteListenerProvider;
    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.context.annotation.DependsOn;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import javax.sql.DataSource;
    import java.util.Properties;
    
    @Configuration
    @EnableTransactionManagement
    public class SubDbConfig {
        
        public static final String DS1_DATASOURCE = "ds1DataSource";
        
        @Value("${spring.second.datasource.xa-data-source-class-name}") String ds1XaDataSourceClassName;
        @Value("${spring.second.datasource.xa-properties.user}") String ds1User;
        @Value("${spring.second.datasource.xa-properties.password}") String ds1Password;
        @Value("${spring.second.datasource.xa-properties.server-name}") String ds1ServerName;
        @Value("${spring.second.datasource.xa-properties.port-number}") String ds1PortNumber;
        @Value("${spring.second.datasource.xa-properties.database-name}") String ds1DatabaseName;
    
        
    
        @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="secondDsl")
        @DependsOn({DS1_DATASOURCE})
        DSLContext dslContext() throws Exception {
            DefaultConfiguration jooqConfiguration = new DefaultConfiguration();
            jooqConfiguration.set( SQLDialect.MYSQL );
            jooqConfiguration.set( dataSource()  );
            jooqConfiguration.set( new DefaultExecuteListenerProvider(new JOOQToSpringExceptionTransformer()) );
            DSLContext dslContext = new DefaultDSLContext(jooqConfiguration);
            return dslContext;
        }
    
    }
    

    Atomikos 설정을 각각 해주게 된다. 여기까지 해주면 끝이 아니라 이제 트랜젝션 설정을 묶어주는 설정이 추가로 필요하다. 

    import com.atomikos.icatch.jta.UserTransactionImp;
    import com.atomikos.icatch.jta.UserTransactionManager;
    import org.jooq.DSLContext;
    import org.jooq.SQLDialect;
    import org.jooq.impl.DefaultConfiguration;
    import org.jooq.impl.DefaultDSLContext;
    import org.jooq.impl.DefaultExecuteListenerProvider;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.DependsOn;
    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;
    
    @Configuration
    @EnableTransactionManagement
    public class MutilTxConfig {
        @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;
        }
    
    
    
    }
    

     이 설정이 되어야만 두 커넥션에 대한 트랜잭션이 정상적으로 묶이게 된다. 

    설정을 완료 했다. 이제 어떻게 사용하는지도 보려고 한다. 사용법은 아주 단순하다. Repository에서 필요한 디비를 @Qualifier 로 지정해서 주입만 해주면 된다. 

     

    아래는 간단하게 spring batch 사용시 사용되는 테이블을 조회하는경우를 샘플로 가져와 보았다. 

    import lombok.extern.slf4j.Slf4j;
    import org.jooq.DSLContext;
    import org.jooq.impl.DSL;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    
    @Slf4j
    @Repository
    public class LoginRepository {
    
        @Autowired
        @Qualifier("firstDsl")
        DSLContext dsl;
    
    
        public int test() throws  Exception{
            return dsl.selectCount().from(DSL.table("BATCH_JOB_EXECUTION") ).limit(1).execute();
        }
    
    }

    jooq 쿼리 자체는 말이 안되지만 쿼리를 보는게 목적이 아니기 때문에 무시하고 넘어간다. 핵심은 @Qualifier("firstDsl") 이다!! 이전 설정에서 bean 이름을 firstDsl 로 정의해놓았다. 그리고 정의된 bean 이름을 dsl 이라는 클래스변수에 주입하여 사용 하면 된다는 것이다. 다른 커넥션을 이용하고자하면 fisrtDsl -> secondDsl 이런식으로 바꿔주면 된다. 필요하다면 둘다 사용해도 무방하다.  

    두개의 데이터베이스 접근가능한것을 이미 확인했고 쿼리도 정상 동작하는것을 확인했다.  아직 트랜잭션 테스트까진는 확인 하지 못했다. 아마 되지 않을까 싶다. 이전 블로그에서의 데이터베이스 연결을 반쪽 짜리 인거 같다. 트랜잭션에 대한 부분까지는 고려못하고 글을 작성했고 추후에 mybatis를 사용하게된다면 이 글을 참조해서 사용해야 할거 같다. 

     

     

    참고 사이트 

    https://stackoverflow.com/questions/32797078/no-rollback-when-using-spring-jooq-postgres-and-activemq

    불러오는 중입니다...

    https://bigzero37.tistory.com/65

    반응형

    'Spring > Spring Boot' 카테고리의 다른 글

    Spring boot - API 서버 만들기 ( ft. Kotlin ) - ( 1 )  (0) 2021.08.30
    Spring boot 시작하기  (0) 2020.03.20
    Spring boot vs Spring Framework  (1) 2019.08.07
    Logback - 특정 이름별로 로그 분리 하기  (0) 2019.06.26
    Spring Cloud  (0) 2019.03.25
Designed by Tistory.