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);
    }