스프링 부트에서 동적 데이터소스 라우팅 구현

다중 데이터소스 환경 구성 및 동적 전환 기술

스프링 부트 프로젝트에서 여러 데이터베이스 연결을 관리하고 실행 시점에 적절한 데이터소스를 선택하려면, 정적 구성만으로는 제한적이므로 동적 라우팅 메커니즘이 필요합니다. 본 문서에서는 AbstractRoutingDataSource를 기반으로 한 유연한 다중 데이터소스 전환 방식을 설명합니다.

  1. 별도 데이터소스 설정 클래스 작성 각 데이터베이스의 연결 정보를 별도의 스프링 빈으로 등록합니다. 구성 파일은 다음과 같이 정의됩니다:
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
  1. 데이터소스 빈 정의 각 데이터베이스에 대한 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();
    }
}
  1. 동적 데이터소스 라우터 설정 라우팅을 담당할 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;
    }
}
  1. 라우팅 키 결정 로직 구현 데이터소스 선택 기준을 결정하는 핵심 메서드입니다. 현재 스레드의 상태에서 데이터소스 식별자를 가져옵니다.
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceSelector.getCurrentType();
    }
}
  1. 데이터소스 식별자 저장용 유틸리티 스레드 단위로 데이터소스 타입을 유지하기 위해 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();
    }
}
  1. 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();
    }
}
  1. 사용자 정의 어노테이션 정의 메서드 수준에서 어떤 데이터소스를 사용할지 명시하기 위한 커스텀 어노테이션입니다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseDataSource {
    String value() default "main";
}
  1. 서비스 계층에서 어노테이션 사용 예시 서비스 메서드에 어노테이션을 추가하여 특정 데이터소스를 사용하도록 지정합니다.
@Service
public class OrderService {

    @UseDataSource("main")
    public List<Order> fetchMainOrders() {
        return orderRepository.findAll();
    }

    @UseDataSource("backup")
    public List<Order> fetchBackupOrders() {
        return backupOrderRepository.findAll();
    }
}

이 방식은 요청이나 사용자 세션, 서비스 계층의 목적에 따라 데이터소스를 자동으로 전환할 수 있어, 분산 데이터베이스 아키텍처, 읽기 전용 복제본 활용, 또는 데이터 분할 환경에서 매우 효과적입니다.

태그: Spring Boot AbstractRoutingDataSource Dynamic Datasource AOP ThreadLocal

7월 11일 05:45에 게시됨