스프링 데이터소스

소규모 시스템에서는 단일 데이터베이스에 의존하지만 조금씩 커지는 시스템에서는 데이터베이스 클러스터에 접근하거나 다수의 데이터베이스에 연결되는 것 같다. 본 글에서는 다중 데이터베이스 연결을 위한 데이터 소스를 어떻게 관리하는지를 다루어보고자 한다. 아래의 영상에서 스프링 개발자 Josh Long 이 데이터소스를 어떻게 다룰 수 있는지에 대해서 다양하게 설명하고 있다.

DataSourceBuilder

스프링 부트에서는 애플리케이션 프로퍼티 파일에 spring.datasource 로 시작하는 속성으로 데이터베이스 연결에 대한 정보를 설정하고 자동 구성을 제공한다. 직접 데이터소스를 생성하거나 다중 데이터베이스에 연결하기 위해서 서로 다른 데이터베이스 연결 정보를 가지는 데이터소스를 사용하고자 하는 경우에 DataSourceBuilderDataSourceProperties 를 사용해볼 수 있다.

스프링 프레임워크에서 기본적으로 사용되는 커넥션 풀 라이브러리는 HikariCP 입니다.

AbstractRoutingDataSource

위 영상에서는 멀티-테넌시 구성으로 서로 다른 리전을 구성한다면 스레드 로컬 변수에 리전 정보를 관리하고 리전에 따른 데이터베이스에 연결하는 예시를 보여주고 있다. 데이터베이스 클러스터로 고가용성의 HA를 구성하는 인프라의 경우에는 AbstractRoutingDataSource 를 활용하여 쓰기 전용 클러스터 엔드포인트와 읽기 전용 엔드포인트를 나누어서 처리할 수 있는 라우터 방식의 데이터소스를 생성할 수 있다.

@AllArgsConstructor
@Configuration
public class DatabaseConfiguration {

    private final DataSourceProperties dataSourceProperties;

    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean("writer")
    public DataSource writerDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean("reader")
    public DataSource readerDataSource() {
        return DataSourceBuilder.create()
                .type(dataSourceProperties.getType())
                .url(dataSourceProperties.determineUrl())
                .username(dataSourceProperties.determineUsername())
                .password(dataSourceProperties.determinePassword())
                .driverClassName(dataSourceProperties.determineDriverClassName())
                .build();
    }

    @Primary
    @Bean
    public DataSource dataSourceRouter() {
        ClusterDataSourceRouter dataSourceRouter = new ClusterDataSourceRouter();
        dataSourceRouter.setTargetDataSources(Map.of(ClusterType.WRITER, writerDataSource()
                , ClusterType.READER, readerDataSource()));
        dataSourceRouter.setDefaultTargetDataSource(writerDataSource());
        return dataSourceRouter;
    }

    public enum ClusterType {
        WRITER, READER
    }

    public static class ClusterDataSourceRouter extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
            if (readOnly) {
                return ClusterType.READER;
            }
            return ClusterType.WRITER;
        }
    }
}

대부분 TransactionSynchronizationManager의 트랜잭션 읽기 속성에 따라 읽기 전용 엔드포인트에 연결되도록 예제를 공유하는 것 같네요.

LazyConnectionDataSourceProxy

스프링의 트랜잭션 처리는 @Transactional 에 진입하는 과정에서 커넥션 연결을 수행하므로 다중 데이터소스에 대한 구성에서는 LazyConnectionDataSourceProxy를 사용하여 커넥션 획득 시점을 늦추는 것이 일반적이다.

@Primary
@Bean
public DataSource dataSourceRouter() {
    ClusterDataSourceRouter dataSourceRouter = new ClusterDataSourceRouter();
    dataSourceRouter.setTargetDataSources(Map.of(ClusterType.WRITER, writerDataSource()
            , ClusterType.READER, readerDataSource()));
    dataSourceRouter.setDefaultTargetDataSource(writerDataSource());
    return new LazyConnectionDataSourceProxy(dataSourceRouter);
}

반드시 커넥션 획득 시점을 늦추는 것이 좋은 방법은 아닐 것이기에 개발자가 시스템 환경에 대한 분석과 판단이 필요합니다.