다중 데이터소스 환경 구성 및 동적 전환 기술
스프링 부트 프로젝트에서 여러 데이터베이스 연결을 관리하고 실행 시점에 적절한 데이터소스를 선택하려면, 정적 구성만으로는 제한적이므로 동적 라우팅 메커니즘이 필요합니다. 본 문서에서는 AbstractRoutingDataSource를 기반으로 한 유연한 다중 데이터소스 전환 방식을 설명합니다.
- 별도 데이터소스 설정 클래스 작성 각 데이터베이스의 연결 정보를 별도의 스프링 빈으로 등록합니다. 구성 파일은 다음과 같이 정의됩니다:
spring:
datasource:
main:
url: jdbc:mysql://localhost:3306/main_db
username: admin
password: secret
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 8
backup:
url: jdbc:mysql://localhost:3306/backup_db
username: admin
password: secret
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 8
- 데이터소스 빈 정의
각 데이터베이스에 대한
DataSource인스턴스를 생성하고, 주 데이터소스로 지정합니다.
@Configuration
@EnableTransactionManagement
public class DatabaseConfig {
@Bean("mainDataSource")
@Primary
@ConfigurationProperties("spring.datasource.main")
public DataSource mainDataSource() {
return DataSourceBuilder.create().build();
}
@Bean("backupDataSource")
@ConfigurationProperties("spring.datasource.backup")
public DataSource backupDataSource() {
return DataSourceBuilder.create().build();
}
}
- 동적 데이터소스 라우터 설정
라우팅을 담당할
DynamicDataSource를 구성하며, 기본 데이터소스와 가능한 대상 목록을 등록합니다.
@Configuration
public class RoutingDataSourceConfig {
@Autowired
@Qualifier("mainDataSource")
private DataSource mainSource;
@Autowired
@Qualifier("backupDataSource")
private DataSource backupSource;
@Bean
public DataSource routingDataSource() {
DynamicDataSource router = new DynamicDataSource();
router.setDefaultTargetDataSource(mainSource);
Map<Object, Object> targetSources = new HashMap<>();
targetSources.put("main", mainSource);
targetSources.put("backup", backupSource);
router.setTargetDataSources(targetSources);
return router;
}
}
- 라우팅 키 결정 로직 구현 데이터소스 선택 기준을 결정하는 핵심 메서드입니다. 현재 스레드의 상태에서 데이터소스 식별자를 가져옵니다.
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceSelector.getCurrentType();
}
}
- 데이터소스 식별자 저장용 유틸리티
스레드 단위로 데이터소스 타입을 유지하기 위해
ThreadLocal를 활용합니다.
public class DataSourceSelector {
private static final ThreadLocal<String> currentType = new ThreadLocal<>();
public static void setCurrentType(String type) {
currentType.set(type);
}
public static String getCurrentType() {
return currentType.get();
}
public static void clear() {
currentType.remove();
}
}
- AOP 기반 데이터소스 전환 처리 메서드 호출 시 자동으로 데이터소스를 전환하기 위해 어드바이스를 적용합니다.
@Aspect
@Component
public class DataSourceSwitcher {
@Pointcut("@annotation(UseDataSource)")
public void dataSourcePointcut() {}
@Before("dataSourcePointcut()")
public void before(JoinPoint jp) {
MethodSignature signature = (MethodSignature) jp.getSignature();
UseDataSource annotation = signature.getMethod().getAnnotation(UseDataSource.class);
DataSourceSelector.setCurrentType(annotation.value());
}
@After("dataSourcePointcut()")
public void after() {
DataSourceSelector.clear();
}
}
- 사용자 정의 어노테이션 정의 메서드 수준에서 어떤 데이터소스를 사용할지 명시하기 위한 커스텀 어노테이션입니다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseDataSource {
String value() default "main";
}
- 서비스 계층에서 어노테이션 사용 예시 서비스 메서드에 어노테이션을 추가하여 특정 데이터소스를 사용하도록 지정합니다.
@Service
public class OrderService {
@UseDataSource("main")
public List<Order> fetchMainOrders() {
return orderRepository.findAll();
}
@UseDataSource("backup")
public List<Order> fetchBackupOrders() {
return backupOrderRepository.findAll();
}
}
이 방식은 요청이나 사용자 세션, 서비스 계층의 목적에 따라 데이터소스를 자동으로 전환할 수 있어, 분산 데이터베이스 아키텍처, 읽기 전용 복제본 활용, 또는 데이터 분할 환경에서 매우 효과적입니다.