How to Configure Multiple DataSource
다중 데이터 소스를 설정하는 방법에 대해 알아보겠습니다.
데이터베이스가 하나만 존재하는 경우에는 간단하게 application.yaml 에 설정 내용만 추가하면 바로 사용이 가능했습니다.
그러나 데이터베이스가 여러 개 존재하는 경우에는 여러 데이터소스를 만들어서 transaction도 잡아주고 DB위치도 다르게 잡아줘야 합니다.
우선은 예시를 위한 디렉토리 구조를 살펴보겠습니다.
디렉토리 구조
중점으로 봐야할 파일은 config.database 디렉토리의 하위 파일입니다.
다중데이터소스 설정을 위한 첫 번째 단계는 application.yaml에 database에 대한 정보를 적어주는 일입니다.
-
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를 지정해주는 파일을 만들어보겠습니다.
-
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에서 발생할 시 지켜질 수 있도록 설정합니다.
-
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 |