들어가기
spring boot의 설정파일을 이용해서 다중 DB을 설정하는 기능이다. 이를 위해서는 순수한 spring boot 제공되는 설정은 1개 DB만 가능하기 때문에 그대로 사용하기에는 한계가 있다. 환경설정을 처리하기 위한 추가적인 작업이 필요하다.
작성자: ospace114@empal.com, http://ospace.tistory.com/
설정파일
먼저 설정파일에 어떻게 설정할려고 하는지 먼저 살펴보자. 사용할 application.properties 내용은 아래와 같다.
spring.datasource.db1.jdbc-url = jdbc:mysql://127.0.0.1:3306/db1?serverTimezone=UTC
spring.datasource.db1.username = foo
spring.datasource.db1.pasword = foopass
spring.datasource.db2.jdbc-url = jdbc:mysql://127.0.0.1:3306/db2?serverTimezone=UTC
spring.datasource.db2.username = foo
spring.datasource.db2.pasword = foopass
DB별로 설정을 가져와서 datasource을 생성하고 세션 팩토리를 만들어서 사용한다. 설정 경로에 “spring.datasource.db1”과 “spring.datasource.db2”을 사용 설정값을 지정한다. ConfigurationProperties을 이용해서 가져올 설정값 위치를 지정한다.
db1 설정
먼저 db1으로 첫 번째 DB이기 때문에 Primary를 지정한다. 기존 설정항목을 재사용했기 때문에 바로 DataSource을 생성할 수 있다.
@Configuration
public class DB1Configuration {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.db1")
public DataSource db1DataSource() {
return DataSourceBuilder.create().build();
}
}
아니면, 설정값을 하나씩 읽어서 생성할 수 도 있다.
@Configuration
public class DB1Configuration {
@Autowired
private Environment env;
@Bean
@Primary
public DataSource db1DataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("spring.datasource.db1.driverClassName"));
dataSource.setUrl(env.getProperty("spring.datasource.db1.jdbc-url"));
dataSource.setUsername(env.getProperty("spring.datasource.db1.username"));
dataSource.setPasswordS(env.getProperty("spring.datasource.db1.password"));
return dataSource;
}
다음으로 세션 팩토리를 만들어주고 매퍼 관련된 설정도 같이 해준다. 매퍼는 mybatis이고 매퍼 클래스 위치는 com.tistory.ospace.repository를 사용했고, alias는 com.tistory.ospace.repsitory.dto를 사용했다.
@Configuration
@MapperScan(
value="com.tistory.ospace.repository.db1",
sqlSessionFactoryRef="db1SqlSessionFactory"
)
public class DB1Configuration {
//중략
@Bean
@Primary
public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db1DataSource") DataSource db1DataSource, ApplicationContext applicationContext applicationContext) throws Exception {
org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
config.setMapUnderscoreToCamelCase(true);
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setMapperLocations(applicationCtx.getResources("classpath:mappers/db1/*.xml"));;
factoryBean.setTypeAliasesPackage("com.tistory.ospace.reposiotry.dto");
factoryBean.setConfiguration(config);
factoryBean.setVfs(SpringBootVFS.class);
return factoryBean.getObject();
}
}
마지막으로 트랜잭션 처리할 트랜잭션 매니저도 만들어준다.
@Configuration
@MapperScan(
value="com.tistory.ospace.repository.db1",
sqlSessionFactoryRef="db1SqlSessionFactory",
transactionManagerRef="db1TransactionManager"
)
public class DB1Configuration {
//중략
@Bean
public DataSourceTransactionManager db1TransactionManager(@Autowired @Qualifier("db1DataSource") DataSource db1DataSource) {
return new DataSourceTransactionManager(db1DataSource);
}
}
db2 설정
db1과 달라진 부분은 Primary가 제외되었고 이름만 db2로 변경됬다.
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
@Configuration
@MapperScan(
value="com.tistory.ospace.repository.db2",
sqlSessionFactoryRef="db2SqlSessionFactory",
transactionManagerRef="db2TransactionManager"
)
public class DBDeviceConfiguration {
// properties의 "spring.datasource.device" 항목의 설정으로 datasource을 생성
@Bean("deviceDataSource")
@ConfigurationProperties("spring.datasource.db2")
public DataSource db2DataSource() {
// LazyConnectionDataSourceProxy 고려
return DataSourceBuilder.create().build();
}
@Bean("deviceSqlSessionFactory")
public SqlSessionFactory db2SqlSessionFactory(@Qualifier("db2DataSource") DataSource db2DataSource, ApplicationContext applicationCtx) throws Exception {
org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
config.setMapUnderscoreToCamelCase(true);
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(db1DataSource);
factoryBean.setMapperLocations(applicationCtx.getResources("classpath:mappers/db2/*.xml"));;
factoryBean.setTypeAliasesPackage("com.tistory.ospace.reposiotry.dto");
factoryBean.setConfiguration(config);
factoryBean.setVfs(SpringBootVFS.class);
return factoryBean.getObject();
}
@Bean
public DataSourceTransactionManager db2TransactionManager(@Autowired @Qualifier("db1DataSource") DataSource db2DataSource) {
return new DataSourceTransactionManager(db2DataSource);
}
}
중간 평가
이 방식은 모든 설정(datasource.mapper, xml)이 분리되서 사용된다. 설정은 같은 곳을 보지만, 근본적으로 매퍼 패키지 위치가 달라지기고 xml에 있는 namespace가 달라지기 때문에 내부적으로 사용하는 쿼리는 서로 다른 폴더로 엄격하게 분리된다. 그러나, DTO의 alias는 공통으로 사용 가능하다.
개선하기
좀더 편하게 사용하기 위해서 코드 재구성 했다.
사용법
- 처음 한개는 기존 datasource설정을 사용하고 두번째 부터 아래 설정을 사용함
- 아래 클래스 파일을 복제
- 클래스 명을 적당히 변경함
- 클래스 안에 mapperName와 packgeName을 수정
- mapperName은 mapper용 XML파일 및 Bean 객체 식별을 위한 정보용
- xml파일은 resource의 mappers 폴더 밑에 있는 mapperName 폴더를 가리킴
- packageName은 Mapper 클래스와 DTO 클래스를 검색하기위한 정보용
- 기본설정은 spring.datasource.{mapperName} 위치에 저장됨
- spring.datasource.bar.jdbc-url = jdbc:mysql://localhost:3306/bar?serverTimezone=Asia/Seoul
- spring.datasource.bar.username = bar_user
- spring.datasource.bar.password = bar_pass
- Mybatis 설정은 고정된 값을 사용함
- camelCase 변환
- mapper 위치 지정됨(mappers/{mapperName}/*.xml)
- Type aliases 패키지 위치 고정됨({packageName}.dto)
개선된 소스코드
package com.foo;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
@Configuration
public class DBBarConfiguration {
private static final String mapperName = "bar";
private static final String packageName = "com.ospace.bar";
@Bean(mapperName+"DataSource")
@ConfigurationProperties("spring.datasource."+mapperName)
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean(mapperName+"SqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier(mapperName+"DataSource") DataSource dataSource, ApplicationContext applicationCtx) throws Exception {
org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
config.setMapUnderscoreToCamelCase(true);
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setConfiguration(config);
factoryBean.setDataSource(dataSource);
factoryBean.setMapperLocations(applicationCtx.getResources("classpath:mappers/"+mapperName+"/*.xml"));;
factoryBean.setTypeAliasesPackage(packageName+".dto");
factoryBean.setVfs(SpringBootVFS.class);
return factoryBean.getObject();
}
@Bean(mapperName+"MapperScannerConfig")
public MapperScannerConfigurer mapperScannerConfig() {
MapperScannerConfigurer ret = new MapperScannerConfigurer();
ret.setBasePackage(packageName);
ret.setSqlSessionFactoryBeanName(mapperName+"SqlSessionFactory");
return ret;
}
@Bean(mapperName+"TransactionManager")
public TransactionManager transactionManager(@Autowired @Qualifier(mapperName+"DataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
트랜잭션 관리
다중 DB에 대한 트랜잭션은 개별 트랜잭션으로 관리해서는 위임할 수 있다. 개별 트랜잭션은 각 datasource에 대한 rollback을 할 수 있지만 다른 datasource에 대해서는 불가능하다.
이를 처리위한 트랜잭션으로 ChainedTransactionManager를 사용해야한다. primary로 등록하면 자동으로 @Transactional에 의해서 사용할 수 있다.
지정된 특정 트랜잭션을 사용하고 싶다면 @Transactional(transactionManager = "fooTransactionManager")으로 지정할 수 있다.
그럼 ChainedTransactionManager 빈객체를 생성해보자.
먼저 dependency을 추가해야한다.
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
compile('org.springframework.data:spring-data-commons')
설정추가
@Configuration
public class TranactionConfig {
@Bean
@Primary
public PlatformTransacctionManager transactionManager(PlatformTransactionManger fooTransactionManager, PlatformTransactionManager barTransactionManager) {
return new ChainedTransactionManager(fooTransactionManager, barTransactionManager);
}
}
다른 트랜잭션 관리자로 JtaTransactionManager가 있다. JTA에서 분산 트랜잭션을 제공한다.
결론
다중 DB 설정하는 방법과 트랜잭선을 구성해보았다. 기본 설정으로만 사용하다가 다중 DB 설정으로 가면 복잡해진다. 물론 다른 설정 방식들도 있지만 처음에 한번 설정하는 DB가 거의 변경되지 않기에 단순하게 사용하기에는 나쁘지 않다. 여러분에게 도움이 되었으면 합니다. 즐프하세요. ospace.
참조
[1] https://supawer0728.github.io/2018/03/22/spring-multi-transaction/
'3.구현 > Java or Kotlin' 카테고리의 다른 글
[spring boot] 다중 DB 사용하기: ApacheShardingSphere 활용 (2) | 2023.11.01 |
---|---|
[spring boot] 다중 DB 사용하기: AbstractionRoutingDataSource 활용 (0) | 2023.10.31 |
[spring boot] jackson대신에 gson으로 사용하기 (0) | 2023.10.27 |
[spring] Spring Framework에서 DB연동 테스트 (0) | 2023.10.26 |
Kotlin 배우기2 - 심화 (0) | 2023.10.18 |