Spring
Spring DataSource 두개 사용하기(Abstract)
당근개발자
2022. 10. 13. 23:19
한번의 request 요청시, spring에서 두 개의 db에서 각각 값을 조회해야하는 경우가 생겼다.
datasource routing이 제대로 설정안돼, 오래동안 삽질한 결과를 적도로 하겠다.
보통은 한 개의 datasource를 사용할 경우, application.properties(yml) 에 db정보를 입력해주면, 자동으로 spring이 datasource정보를 등록을 해준다.
하지만, 멀티 datasource를 사용해야 할 경우, 수동으로 등록을 해주어야한다.
/resources/application.properties
// application.properties
datasource.main.driver-class-name=com.mysql.cj.jdbc.Driver
datasource.main.url=jdbc:mysql://xxx.xx.xx.xx:3306/schema?useLegacyDatetimeCode=false&serverTimezone=UTC
datasource.main.username=root
datasource.main.password=1234
datasource.front.driver-class-name=com.mysql.cj.jdbc.Driver
datasource.front.url=jdbc:mysql://xxx.xx.xx.xx:3306/schema?useLegacyDatetimeCode=false&serverTimezone=UTC
datasource.front.username=root
datasource.front.password=1234
필자는 datasource 다음 main과 front로 구분해서 지정하였다.
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
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.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.zaxxer.hikari.HikariDataSource;
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
// datasource 설정을 적용할 상위 패키지
basePackages = "com.partridge.tools.dhst",
entityManagerFactoryRef = "entityManager",
transactionManagerRef = "transactionManager"
)
public class DatabaseConfig {
@Bean(name="datasourceConfig")
@Primary
public DataSource dataSource() {
DataSourceRouting dataSourceRouting = new DataSourceRouting();
dataSourceRouting.setTargetDataSources(targetDataSources());
dataSourceRouting.setDefaultTargetDataSource(mainDataSource());
return dataSourceRouting;
}
private Map<Object, Object> targetDataSources() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DatabaseEnum.FRONT, frontDataSource());
targetDataSources.put(DatabaseEnum.MAIN, mainDataSource());
return targetDataSources;
}
@Bean
@ConfigurationProperties("datasource.main") // application.properties에 있는 정보로, datasource 기본 정보 생성
public DataSourceProperties mainDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
public DataSource mainDataSource() {
return mainDataSourceProperties()
.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
@Bean
@ConfigurationProperties("datasource.front")
public DataSourceProperties frontDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
public DataSource frontDataSource() {
return frontDataSourceProperties()
.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
@Bean(name = "entityManager")
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(
EntityManagerFactoryBuilder builder) {
return builder.dataSource(dataSource()).packages("com.partridge.tools.dhst")
.build();
}
// datasource 관리할 트랜잭션 매니저 등록
@Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager(
@Autowired @Qualifier("entityManager") LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
JpaTransactionManager tm = new JpaTransactionManager(entityManagerFactoryBean.getObject());
tm.setDataSource(dataSource());
return tm;
}
}
datasource 등록을 위한 databaseconfig 자바를 작성해준다.
그리고 multi db를 routing을 위한 로직을 작성한다.
/RoutingDatabaseContextHolder
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
@Component
public class RoutingDatabaseContextHolder {
private static ThreadLocal<DatabaseEnum> CONTEXT= new ThreadLocal<>();
public static void set(DatabaseEnum clientDatabase) {
Assert.notNull(clientDatabase, "RoutingDatabase cannot be null");
CONTEXT.set(clientDatabase);
}
public static DatabaseEnum getClientDatabase() {
System.out.println("context : "+CONTEXT.get());
if(CONTEXT.get()==null|| CONTEXT.get() ==DatabaseEnum.MAIN){
return DatabaseEnum.MAIN;
}
return DatabaseEnum.FRONT;
}
public static void clear() {
CONTEXT.remove();
}
}
public enum DatabaseEnum {
MAIN,FRONT;
}
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DataSourceRouting extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return RoutingDatabaseContextHolder.getClientDatabase();
}
}
@GetMapping("/pagelog")
public ResponseEntity<?> retrievePageLong(Pageable pageable) throws SQLException{
RoutingDatabaseContextHolder.set(DatabaseEnum.MAIN);
System.out.println("main db method CALL BEFORE");
Result<UserResponseDto> retrieveUserByProject = adminService.retrieveUserByProject(4, 4, pageable);
System.out.println("main :" +retrieveUserByProject.getCount());
RoutingDatabaseContextHolder.set(DatabaseEnum.FRONT);
platformTransactionManager.getTransaction(new DefaultTransactionDefinition());
System.out.println("FRONT db method CALL BEFORE");
List<PageLog> retrievePageLog = pageLogService.retrievePageLog(pageable);
RoutingDatabaseContextHolder.clear();
return ResponseEntity.ok().body(retrievePageLog);
}