이번 포스트에서는 SpringBoot에서 다중 데이터베이스 연결을 사용하는 방법에 대해서 설명합니다.
본 포스트에서는 MyBatis의 상세 설정 부분은 아래의 포스트와 중복되어 불필요한 부분은 설명하지 않습니다.
1. 데이터베이스 연결 설정
Maven 종속성 추가
<!-- MyBatis and Database -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.0</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis sql pretty -->
<dependency>
<groupId>org.bgee.log4jdbc-log4j2</groupId>
<artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
<version>1.16</version>
</dependency>
Application 환경변수 추가
application.properties 파일에 데이터베이스 연결 정보를 다음을 참고하여 작성합니다. 저는 Mapper 및 MyBatis 환경설정도 추가적으로 정의하였습니다.
spring.db1.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.db1.datasource.jdbc-url=jdbc:log4jdbc:mysql://__db1_host__:__db1_port__/__db1_name__
spring.db1.datasource.username=__db1_user__
spring.db1.datasource.password=__db1_password__
spring.db1.datasource.mapper-locations=classpath:/mybatis/db1/**/*.xml
spring.db1.datasource.mybatis-config=classpath:/mybatis/mybatis-config.xml
spring.db2.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.db2.datasource.jdbc-url=jdbc:log4jdbc:mysql://__db2_host__:__db2_port__/__db2_name__
spring.db2.datasource.username=__db2_user__
spring.db2.datasource.password=__db2_password__
spring.db2.datasource.mapper-locations=classpath:/mybatis/db2/**/*.xml
spring.db2.datasource.mybatis-config=classpath:/mybatis/mybatis-config.xml
2. 데이터베이스 환경설정
데이터베이스1과 2의 차이는 데이터베이스 1(기본 데이터베이스)에서 @Primary 어노테이션이 붙는 것 외에는 다르지 않습니다. db1 => db2, Db1 => Db2 외에는 동일합니다.
HikariCP 설정의 경우 SpringBoot 2.x에서는 기본으로 추가되었습니다. 따라서 아래의 코드에서는 주석 처리하였습니다.
데이터베이스 1 설정
package multi.database.db1.config;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
//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.context.annotation.Primary;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
@Configuration
@PropertySource("classpath:/application.properties")
@MapperScan(value="multi.database.db1.dao", sqlSessionFactoryRef="db1SqlSessionFactory")
public class Db1Config {
private static final Logger logger = LoggerFactory.getLogger(Db1Config.class);
@Autowired
private ApplicationContext applicationContext;
@Value("${spring.db1.datasource.mapper-locations}")
private String mapperLocations;
@Value("${spring.db1.datasource.mybatis-config}")
private String configPath;
// @Bean(name = "db1HikariConfig")
// @Primary
// @ConfigurationProperties(prefix = "spring.db1.datasource")
// public HikariConfig db1HikariConfig() {
// return new HikariConfig();
// }
//
// @Bean(name = "db1DataSource")
// @Primary
// public DataSource db1DataSource() {
// DataSource dataSource = new HikariDataSource(db1HikariConfig());
// logger.info("OAuth Datasource : {}", dataSource);
// return dataSource;
// }
@Bean(name = "db1DataSource")
@Primary
@ConfigurationProperties(prefix = "spring.db1.datasource")
public DataSource db1DataSource() {
DataSource dataSource = DataSourceBuilder.create().build();
logger.info("Datasource : {}", dataSource);
return dataSource;
}
@Bean(name = "db1SqlSessionFactory")
@Primary
public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(mapperLocations));
// Properties properties = new Properties();
// properties.put("mapUnderscoreToCamelCase", true);
// sqlSessionFactoryBean.setConfigurationProperties(properties);
//Mybatis config파일 위치
Resource myBatisConfig = new PathMatchingResourcePatternResolver().getResource(configPath);
sqlSessionFactoryBean.setConfigLocation(myBatisConfig);
return sqlSessionFactoryBean.getObject();
}
@Bean(name = "db1SqlSessionTemplate")
@Primary
public SqlSessionTemplate db1SqlSessionTemplate(@Qualifier("db1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
데이터베이스 2 설정
package multi.database.db2.config;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.context.annotation.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
@Configuration
@PropertySource("classpath:/application.properties")
@MapperScan(value="multi.database.db2.dao", sqlSessionFactoryRef="db2SqlSessionFactory")
public class Db2Config {
private static final Logger logger = LoggerFactory.getLogger(Db2Config.class);
@Autowired
private ApplicationContext applicationContext;
@Value("${spring.db2.datasource.mapper-locations}")
private String mapperLocations;
@Value("${spring.db2.datasource.mybatis-config}")
private String configPath;
// @Bean(name = "db2HikariConfig")
// @ConfigurationProperties(prefix = "spring.db2.datasource")
// public HikariConfig db2HikariConfig() {
// return new HikariConfig();
// }
//
// @Bean(name = "db2DataSource")
// public DataSource db2DataSource() {
// DataSource dataSource = new HikariDataSource(db2HikariConfig());
//// DataSource dataSource = DataSourceBuilder.create().build();
// logger.info("Resource Datasource : {}", dataSource);
// return dataSource;
// }
@Bean(name = "db2DataSource")
@ConfigurationProperties(prefix = "spring.db2.datasource")
public DataSource db2DataSource() {
DataSource dataSource = DataSourceBuilder.create().build();
logger.info("Datasource : {}", dataSource);
return dataSource;
}
@Bean(name = "db2SqlSessionFactory")
public SqlSessionFactory db2SqlSessionFactory(@Qualifier("db2DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(mapperLocations));
// Properties properties = new Properties();
// properties.put("mapUnderscoreToCamelCase", true);
// sqlSessionFactoryBean.setConfigurationProperties(properties);
//Mybatis config파일 위치
Resource myBatisConfig = new PathMatchingResourcePatternResolver().getResource(configPath);
sqlSessionFactoryBean.setConfigLocation(myBatisConfig);
return sqlSessionFactoryBean.getObject();
}
@Bean(name = "db2SqlSessionTemplate")
public SqlSessionTemplate db2SqlSessionTemplate(@Qualifier("db2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
3. 데이터베이스 Mapper.xml 기반 연결
데이터베이스 Service 객체
@Qualifier 어노테이션을 사용하여 사용하려는 DAO객체를 선택할 수 있게 합니다.
package multi.database.service;
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.stereotype.Service;
import multi.database.db1.dao.Db1DAO;
import multi.database.db2.dao.Db2DAO;
import multi.database.model.Model;
@Service("withDaoService")
public class WithDaoService {
private static final Logger logger = LoggerFactory.getLogger(WithDaoService.class);
@Autowired
@Qualifier("db1DAO")
private Db1DAO db1DAO;
@Autowired
@Qualifier("db2DAO")
private Db2DAO db2DAO;
public int moveDb1toDb2(String modelId) {
Model model = db1DAO.findModelByModelId(modelId);
return db2DAO.saveModel(model);
}
}
데이터베이스 1 DAO
package multi.database.db1.dao;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Repository;
import multi.database.model.Model;
@Repository("db1DAO")
public class Db1DAO {
@Autowired
@Qualifier("db1SqlSessionTemplate")
private SqlSessionTemplate sqlSession;
public Model findModelByModelId(String modelId) {
return sqlSession.selectOne("model.findModelByModelId", modelId);
}
public int saveModel(Model model) {
return sqlSession.insert("model.saveModel", model);
}
}
데이터베이스 2 DAO
package multi.database.db2.dao;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Repository;
import multi.database.model.Model;
@Repository("db2DAO")
public class Db2DAO {
@Autowired
@Qualifier("db2SqlSessionTemplate")
private SqlSessionTemplate sqlSession;
public Model findModelByModelId(String modelId) {
return sqlSession.selectOne("model.findModelByModelId", modelId);
}
public int saveModel(Model model) {
return sqlSession.insert("model.saveModel", model);
}
}
데이터베이스 mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="model">
<select id="findModelByModelId" resultType="some.package.model.Model">
<![CDATA[
SELECT
*
FROM
multi_db_model
WHERE
model_id = #{modelId}
]]>
</select>
<insert id="saveModel" keyProperty="model_id">
REPLACE INTO multi_db_model
(model_id)
VALUES
(#{modelId})
</insert>
</mapper>
4. 데이터베이스 Mapper 객체 기반 연결
데이터베이스 Service 객체
데이터베이스의 연결 정보는 Configuration 객체의 상단에 설정된 @MapperScan 어노테이션을 통해서 이루어집니다. Mapper객체를 사용할 패키지를 구별하여 사용해야 합니다.
package multi.database.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import multi.database.db1.dao.Db1Mapper;
import multi.database.db2.dao.Db2Mapper;
import multi.database.model.Model;
@Service("withMapperService")
public class WithMapperService {
private static final Logger logger = LoggerFactory.getLogger(WithMapperService.class);
@Autowired
Db1Mapper db1Mapper;
@Autowired
Db2Mapper db2Mapper;
public int moveDb1toDb2(String modelId) {
Model model = db1Mapper.findModelByModelId(modelId);
return db2Mapper.saveModel(model);
}
}
데이터베이스 1 Mapper 객체
package multi.database.db1.dao;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import multi.database.model.Model;
public interface Db1Mapper {
@Select("SELECT * FROM multi_db_model WHERE model_id = #{modelId}")
public Model findModelByModelId(String modelId);
@Insert("INSERT INTO multi_db_model (model_id) VALUES (#{modelId})")
public int saveModel(Model model);
}
데이터베이스 2 Mapper 객체
package multi.database.db2.dao;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import multi.database.model.Model;
public interface Db2Mapper {
@Select("SELECT * FROM multi_db_model WHERE model_id = #{modelId}")
public Model findModelByModelId(String modelId);
@Insert("INSERT INTO multi_db_model (model_id) VALUES (#{modelId})")
public int saveModel(Model model);
}
글을 마치며
Mapper.xml 기반과 Mapper 객체 기반으로 여러 개의 데이터베이스를 연결하는 방법 중에 어떤 것이 좋을지는 상황에 따라 달라질 것입니다. 예를 들어 만들려는 코드의 최종 결과물이 제품 형태로 제공되어 SQL 문의 보호가 필요하다면 Mapper 객체 기반이 유리할 것입니다. 그리고, 데이터베이스 종류에 따라 연결이 변경되는 상황이라면 Mapper.xml을 application.properties의 설정에 따라 변경해줄 수 있는 Mapper.xml 기반이 유리합니다.
데이터베이스 Configuration에서 @MapperScan의 패키지 경로를 런타임에 수정이 가능하다면 여러 데이터베이스 지원과 제품 형태의 결과물을 사용할 수 있습니다. 아직은 동적으로 매핑하는 방법을 찾지 못하였습니다.
@2020.02.18 데이터베이스를 동적 Mapper를 설정할 수 있는 방법을 찾았습니다. Spring Boot 애플리케이션 실행 시 JVM 파라미터에 환경변수를 추가합니다. (application.properties는 값은 이미 MapperScan가 처리된 후에 참조하여 사용할 수 없습니다.)
java -Ddatabase.type.db1=mysql -jar archive.jar
데이터베이스 설정 코드의 MapperScan을 다음을 참고하여 수정합니다.
package multi.database.db1.config;
......
@Configuration
@PropertySource("classpath:/application.properties")
@MapperScan(value="multi.database.db1.mapper.${database.type.db1:oracle}", sqlSessionFactoryRef="db1SqlSessionFactory")
public class Db1Config {
private static final Logger logger = LoggerFactory.getLogger(Db1Config.class);
......
}
참고사이트
- 스프링 부트 2.0의 default DBCP, hikariCP가 그렇게 빠르다던데?
https://jeong-pro.tistory.com/162 - Spring Boot 애플리케이션에서 2개 이상의 Datasource 운용하기
https://www.holaxprogramming.com/2015/10/10/spring-boot-multi-datasources/
소스코드
본 포스트 관련 소스코드는 여기에서 다운로드 가능합니다.
'Tips, Tricks > Java, Spring Framework' 카테고리의 다른 글
Create a File Hash in Java(자바로 파일 해쉬 만들기) (0) | 2020.11.13 |
---|---|
Converting Java ImageIO.write to ImageWriter(Java ImageIO.write를 ImageWriter로 전환하기) (0) | 2020.11.09 |
Spring Boot 프로젝트에 외부 jar 추가 방법(method of including external jar on spring boot maven project) (2) | 2020.04.23 |
TCP Socket Forwarding(Tunneling) by Java Socket (0) | 2020.02.18 |
Spring Boot - MyBatis & log4jdbc 설정 (2) | 2019.11.21 |