본문 바로가기

JAVA/Spring Framework

[Spring] 다중 데이터소스 설정(Multiple Datasource JPA, Mybatis)

How to Configure Multiple DataSource
 
다중 데이터 소스를 설정하는 방법에 대해 알아보겠습니다.
 
데이터베이스가 하나만 존재하는 경우에는 간단하게 application.yaml 에 설정 내용만 추가하면 바로 사용이 가능했습니다.
 
그러나 데이터베이스가 여러 개 존재하는 경우에는 여러 데이터소스를 만들어서 transaction도 잡아주고 DB위치도 다르게 잡아줘야 합니다.
 
우선은 예시를 위한 디렉토리 구조를 살펴보겠습니다.
 
디렉토리 구조
 
중점으로 봐야할 파일은 config.database  디렉토리의 하위 파일입니다. 
 
다중데이터소스 설정을 위한 첫 번째 단계는 application.yaml에 database에 대한 정보를 적어주는 일입니다.
 
  1. application.yaml에 DataSource 여러 개 입력하기
 
spring:
  datasource:
    testdb1:
      maximum-pool-size: 4
      jdbc-url: jdbc:postgresql://localhost:5432/test
      username: postgres
      password:
      driver-class-name: org.postgresql.Driver
 
 
    testdb2:
      maximum-pool-size: 4
      jdbc-url: jdbc:postgresql://localhost:5432/test2
      username: postgres
      password:
      driver-class-name: org.postgresql.Driver
 
 
이제 2개의 데이터베이스에 접근하기 위한 datasource를 지정해주는 파일을 만들어보겠습니다.
 
  1. JPA DataSource 설정하기
 
 
import com.zaxxer.hikari.HikariDataSource;
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.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
 
 
@Configuration
@ConfigurationProperties(prefix = "spring.datasource." + "testdb1")
@EnableJpaRepositories(
        entityManagerFactoryRef = "testdb1" + "EntityManagerFactory",
        transactionManagerRef = "testdb1" + "TransactionManager",
        basePackages = {DatabaseConfig.BASE_ENTITY_PACKAGE_PREFIX + ".test"}
)
public class TestDb1 extends DatabaseConfig {
    final String name = "testdb1";
 
 
    @Bean(name = name + "DataSource")
    @Primary
    public DataSource dataSource() {
        return new LazyConnectionDataSourceProxy(new HikariDataSource(this));
    }
 
    /* -----------------JPA 셋팅------------------------------------- */
    @Bean(name = name + "EntityManagerFactory")
    @Primary
    public EntityManagerFactory entityManagerFactory(@Qualifier(name + "DataSource") DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setDataSource(dataSource);
        factory.setPackagesToScan("com.cafe24.cmc.domain.test");
        factory.setPersistenceUnitName(name);        
        setConfigureEntityManagerFactory(factory);
 
        return factory.getObject();
    }
 
 
    @Bean(name = name + "TransactionManager")
    @Primary
    public PlatformTransactionManager transactionManager(@Qualifier(name + "EntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(entityManagerFactory);
 
        return tm;
    }
}
 
  • @ConfigurationProperties(prefix = "spring.datasource." + "testdb1") : application.yaml에서 어떤 properties를 읽을 지 지정합니다.
  • @EnableJpaRepositories(...) : Jpa에 관한 설정 및 파일의 위치는 어디에 있는 지 명시합니다. 
  • @Bean(...) : 각 메소드들을 빈으로 등록하며 빈의 이름을 지정하여 동일한 클래스일지라도 @Qulifier를 통한 선택적 주입이 가능하게 합니다.
  • @Primary : 첫 째 DB소스는 무엇인 지 명시합니다. 이 후 생성될 DB는 @Primary가 없어야 합니다. 
 
차례대로 살펴보면 다음과 같습니다.
 
    @Bean(name = name + "DataSource")
    @Primary
    public DataSource dataSource() {
        return new LazyConnectionDataSourceProxy(new HikariDataSource(this));
    }
 
DataSource Interface를 상속받아 구현된 DataSource를 생성합니다. 
DataSource interface로 만들어질 DataSource는 여러개가 될 것이므로 @Bean(name="")을 통해 해당 객체를 Context에 등록할 때 bean의 이름을 지정합니다. 
채택된 방법은 LazyConnectionDataSourceProxy이며 단순하게 HikariDataSource만 return해도 무방합니다. 
 
    @Bean(name = name + "EntityManagerFactory")
    @Primary
    public EntityManagerFactory entityManagerFactory(@Qualifier(name + "DataSource") DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setDataSource(dataSource);
        factory.setPackagesToScan("com.cafe24.cmc.domain.test");
        factory.setPersistenceUnitName(name);
        setConfigureEntityManagerFactory(factory);
 
        return factory.getObject();
    }
 
JPA 사용 시 EntityManager가 작업을 해주지만 실제로 사용하는 우리의 입장에서는 Spring Data JPA를 사용하기에 직접 볼 일은 없을 것 입니다.
다만, EntityManagerFactory에 필요한 추가 설정 작업을 해주어 factory.setPackagesToScan("com.cafe24.cmc.domain.test"); 에 내용을 스캔할 수 있도록 설정해줍니다. 
 
    @Bean(name = name + "TransactionManager")
    @Primary
    public PlatformTransactionManager transactionManager(@Qualifier(name + "EntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(entityManagerFactory);
 
        return tm;
    }
 
마지막으로 TransactionManger를 설정해주어 transaction이 하나의 datasource에서 발생할 시 지켜질 수 있도록 설정합니다.
 
 
  1. Mybatis 설정 추가 
 
@MapperScan(
        basePackages = {DatabaseConfig.BASE_MAPPER_PACKAGE_PREFIX + ".test"}
)
public class TestDb1 extends DatabaseConfig {
 
....
 
 
/* -----------------mybatis 셋팅------------------------------------- */
@Bean(name = name + "SessionFactory")
@Primary
public SqlSessionFactory sqlSessionFactory(@Qualifier(name + "DataSource") DataSource dataSource) throws Exception {
    SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
    setConfigureSqlSessionFactory(sessionFactoryBean, dataSource);
 
    return sessionFactoryBean.getObject();
}
 
 
@Bean(name = name + "SqlSessionTemplate")
@Primary
public SqlSessionTemplate firstSqlSessionTemplate(@Qualifier(name + "SessionFactory") SqlSessionFactory sqlSessionFactory) {
    return new SqlSessionTemplate(sqlSessionFactory);
}
 
Mybatis의 경우 SessionFactory를 통해 SqlSession이 Datasource를 물고 있습니다. 
따라서 Mapper가 존재하는 파일의 위치를 읽을 수 있도록 지정해주고 JPA 셋팅과 동일하게 datasource를 설정해줍니다. 
 
전체 코드는 다음과 같습니다.
파일명 : TestDb1
 
import com.zaxxer.hikari.HikariDataSource;
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.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
 
 
@Configuration
@ConfigurationProperties(prefix = "spring.datasource." + "testdb1")
@EnableJpaRepositories(
        entityManagerFactoryRef = "testdb1" + "EntityManagerFactory",
        transactionManagerRef = "testdb1" + "TransactionManager",
        basePackages = {DatabaseConfig.BASE_ENTITY_PACKAGE_PREFIX + ".test"}
)
@MapperScan(
        basePackages = {DatabaseConfig.BASE_MAPPER_PACKAGE_PREFIX + ".test"}
)
public class TestDb1 extends DatabaseConfig {
    final String name = "testdb1";
 
 
    @Bean(name = name + "DataSource")
    @Primary
    public DataSource dataSource() {
        return new LazyConnectionDataSourceProxy(new HikariDataSource(this));
    }
 
 
    /* -----------------mybatis 셋팅------------------------------------- */
    @Bean(name = name + "SessionFactory")
    @Primary
    public SqlSessionFactory sqlSessionFactory(@Qualifier(name + "DataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        setConfigureSqlSessionFactory(sessionFactoryBean, dataSource);
 
 
        return sessionFactoryBean.getObject();
    }
 
 
    @Bean(name = name + "SqlSessionTemplate")
    @Primary
    public SqlSessionTemplate firstSqlSessionTemplate(@Qualifier(name + "SessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
 
 
    /* -----------------JPA 셋팅------------------------------------- */
    @Bean(name = name + "EntityManagerFactory")
    @Primary
    public EntityManagerFactory entityManagerFactory(@Qualifier(name + "DataSource") DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setDataSource(dataSource);
        factory.setPackagesToScan("com.cafe24.cmc.domain.test");
        factory.setPersistenceUnitName(name);
        setConfigureEntityManagerFactory(factory);
 
 
        return factory.getObject();
    }
 
 
    @Bean(name = name + "TransactionManager")
    @Primary
    public PlatformTransactionManager transactionManager(@Qualifier(name + "EntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(entityManagerFactory);
        return tm;
    }
}
 
 
두 번째 데이터소스를 설정하기 위해선 동일한 작업을 해줍니다. 
파일명 : TestDb2
 
 
import com.zaxxer.hikari.HikariDataSource;
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.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
 
 
@Configuration
@ConfigurationProperties(prefix = "spring.datasource." + "testdb2")
@EnableJpaRepositories(
        entityManagerFactoryRef = "testdb2" + "EntityManagerFactory",
        transactionManagerRef = "testdb2" + "TransactionManager",
        basePackages = {DatabaseConfig.BASE_ENTITY_PACKAGE_PREFIX + ".test2"}
)
@MapperScan(
        basePackages = {DatabaseConfig.BASE_MAPPER_PACKAGE_PREFIX + ".test2"}
)
public class TestDb2 extends DatabaseConfig {
    final String name = "testdb2";
 
 
    @Bean(name = name + "DataSource")
    public DataSource dataSource() {
        return new LazyConnectionDataSourceProxy(new HikariDataSource(this));
    }
 
 
    /* -----------------mybatis 셋팅------------------------------------- */
    @Bean(name = name + "SessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier(name + "DataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        setConfigureSqlSessionFactory(sessionFactoryBean, dataSource);
 
 
        return sessionFactoryBean.getObject();
    }
 
 
    @Bean(name = name + "SqlSessionTemplate")
    public SqlSessionTemplate firstSqlSessionTemplate(@Qualifier(name + "SessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
 
 
    /* -----------------JPA 셋팅------------------------------------- */
    @Bean(name = name + "EntityManagerFactory")
    public EntityManagerFactory entityManagerFactory(@Qualifier(name + "DataSource") DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setDataSource(dataSource);
        factory.setPackagesToScan("com.cafe24.cmc.domain.test2");
        factory.setPersistenceUnitName("testdb2");
        setConfigureEntityManagerFactory(factory);
 
 
        return factory.getObject();
    }
 
 
    @Bean(name = name + "TransactionManager")
    public PlatformTransactionManager transactionManager(@Qualifier(name + "EntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(entityManagerFactory);
 
 
        return tm;
    }
}
 
 
동일한 코드가 반복됨에 따라 설정을 조금 더 쉽게 관리하기 위하여 설정 파일을 분리하였습니다.
파일명 : DatabaseConfig
 
 
import com.google.common.collect.ImmutableMap;
import com.zaxxer.hikari.HikariConfig;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
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.io.IOException;
 
public abstract class DatabaseConfig extends HikariConfig {
    public static final String BASE_MAPPER_PACKAGE_PREFIX = "com.cafe24.cmc.mapper";
    public static final String BASE_ENTITY_PACKAGE_PREFIX = "com.cafe24.cmc.domain";
 
    protected void setConfigureEntityManagerFactory(LocalContainerEntityManagerFactoryBean factory) {
        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        factory.setJpaVendorAdapter(vendorAdapter);
        factory.setJpaPropertyMap(ImmutableMap.of(
                "hibernate.hbm2ddl.auto", "update",
                "hibernate.dialect", "org.hibernate.dialect.PostgreSQL10Dialect",
                "hibernate.show_sql", "true",
                "hibernate.format_sql", "true"
        ));
        factory.afterPropertiesSet();
    }
 
    protected void setConfigureSqlSessionFactory(SqlSessionFactoryBean sessionFactoryBean, DataSource dataSource) throws IOException {
        sessionFactoryBean.setDataSource(dataSource);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sessionFactoryBean.setMapperLocations(resolver.getResources("classpath*:mybatis/mapper/**/*.xml"));
    }
}
 
ImmutableMap.of() 를 사용하기 위해서는 다음의 내용을 build.gradle에 추가합니다.
 
// https://mvnrepository.com/artifact/com.google.guava/guava
compile group: 'com.google.guava', name: 'guava', version: '28.1-jre'
 
 

'JAVA > Spring Framework' 카테고리의 다른 글

[Spring] 간단한 TestCase 만들기  (0) 2020.01.07
[Spring] Controller와 Service 생성하기  (0) 2020.01.07
GSON 과 JSON 차이 및 변형  (0) 2019.07.22
[Spring Framework] Security 설정 및 원리  (0) 2019.07.17
[Spring] Logging  (0) 2019.05.21