Spring multi datasource 구성

h블로그·2022년 1월 13일
0

multidatasource

목록 보기
1/2

대부분 서비스는 조회(Read)를 하는 경우가 현저히 많다.
write를 담당하는 Master 인스턴스와 read를 담당하는 Slave 인스턴스를 나누어 성능을 향상시킬수 있다.
RDS Read Replica 를 이용하면 그런 환경을 구성할 수 있으며, 이 글에서 얘기할 부분은 어플리케이션 단에서 multi datasource를 구성하는 방법이다.

db 인스턴스

  • Master db > write 담당
  • Slave db > read 담당

multi datasource 구성할 방법

관련 library가 있는지?

지원하는 라이브러리가 없었다.

jdbc driver로 설정

mysql은 지원하는 jdbc driver 가 있었으나, 다른 db는 없다.
내가 하고있던 프로젝트는 postgres db 였다.

db proxy ?

어떤 블로그에서 이런 방법이 있다고 보앗으나, multi datasource를 지원하는 db proxy 레퍼런스를 찾지 못했다. 다른 레퍼런스가 많은 방법이 있었음으로, 선택할 이유가 없었다.

어플리케이션에서 config 구현

spring 에서 대부분 사용하는 방법이였으며, 이미 예제를 가지고 있었다.
이 방법을 사용했다.

고려할 점

단순히 insert, select 문으로 사용할 db를 선택하게 나누면 한 트랜잭션에서 저장&조회가 일어났을때 데이터가 일치하지 않는 문제가 발생할 수 있음으로, 트랜잭션 단위로 db를 선택하여 붙도록 해야한다.

source

application.yml

spring:
  datasource:
    master:
      # db 접속 정보들
      jdbc-url: 
      username:
      password:
      driver-class-name:
    slave: 
      # db 접속 정보들
      jdbc-url: 
      username:
      password:
      driver-class-name:

예제임으로 접속정보는 비워두었다.

DataSourceConfig.java

master, slave datasource 연결

@Configuration
public class DataSourceConfig {
    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix="spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }
    
    @Bean(name = "slaveDataSource")
    @ConfigurationProperties(prefix="spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }
}

application.yml 에 작성한 2개의 db가 자동으로 잡히지 않음으로 datasource 연결을 작성한다.

@Transactional 속성에 따라서 data source type 반환하도록 메소드 재정의

public enum DataSourceType {
    MASTER,
    SLAVE
}
class RoutingDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? DataSourceType.SLAVE : DataSourceType.MASTER;
        }
    }

AbstractRoutingDataSource 상속받아서 determineCurrentLookupKey() 를 작성하면 readOnly 값에 따라서 datasource 를 라우팅하도록 재정의할수 있다.
내부 클래스나 외부 클래스로 작성하면 된다. 나는 운영 환경에서만 멀티 구성이 필요해서 DataSourceConfig.java 안에 내부 클래스로 재정의했다.

routng datasource 구성

@DependsOn({"masterDataSource","slaveDataSource"})
@Bean(name = "routingDataSource")
public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                    @Qualifier("slaveDataSource") DataSource slaveDataSource) {
    RoutingDataSource routingDataSource = new RoutingDataSource();

    Map<Object, Object> dataSourceMap = new HashMap<>();
    dataSourceMap.put(DataSourceType.MASTER, masterDataSource);
    dataSourceMap.put(DataSourceType.SLAVE, slaveDataSource);

    routingDataSource.setTargetDataSources(dataSourceMap);
    routingDataSource.setDefaultTargetDataSource(masterDataSource);

    return routingDataSource;
}

재정의한 RoutingDataSource 클래스에 위에서 정의한 data source를 넣고, default datasource 는 master 로 설정한다.
처음에 어노테이션은 @Bean만 넣었었는데, 순환 참조가 발생해서 @DependsOn 어노테이션을 함께 달아주었다.

@DependsOn({"masterDataSource","slaveDataSource"})
의존관계에 있는 masterDataSource, slaveDataSource가 먼저 생성된다.

dataSource

Spring은 기본적으로 트랜잭션에 진입하는 순간 설정된 datasource를 가져온다.
기본 설정을 사용하는 경우, 이미 datasource 가 설정되어 분기로 가져오는 것이 불가능하다.

LazyConnectionDataSourceProxy 를 이용하면 필요한 시점에만 커넥션을 점유함으로 분기한 후 선택된 datasource 로 커넥션 풀을 점유한다.

@Primary
@DependsOn("routingDataSource")
@Bean(name = "dataSource")
public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {
    return new LazyConnectionDataSourceProxy(routingDataSource);
}

@DependsOn("routingDataSource")부분은 마찬가지로 순환참조 문제를 해결한다.

온라인에서 본 여러 예제들은 여기까지 작성하면 트랜잭션 속성에 따라 multi datasource 가 되었다.
그러나 진행중인 프로젝트는 여기까지 진행하면 entityManagerFactory 를 찾지 못하는 에러로 구동이 안됐다.

entity manager factory 설정

@Configuration
@RequiredArgsConstructor
public class DataSourceConfig {
    private final Environment env;

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder,
                                                                       DataSource dataSource) {
        final HashMap<String, Object> properties = new HashMap<>();

        properties.put("hibernate.hbm2ddl.auto", env.getProperty("spring.jpa.hibernate.ddl-auto"));
        // 자동으로 잡히지 않는 property 넣어줄 부분있으면 작성 ..

        LocalContainerEntityManagerFactoryBean build = builder
                .dataSource(dataSource)
                .packages("com.test.domain") // entity 패키지 위치
                .properties(properties)
                .build();

        return build;
    }
}

EntityManagerFactoryBuilder, DataSource 를 파라미터로 받으려면 위에서 구현한 dataSource() 빈등록 메소드에 @Primary 어노테이션을 달아주어야 한다.

테스트

local에서 docker를 이용해서 두개의 db를 띄워서 테스트를 진행했다.

profile
😎🙈🙈🙈🤓

0개의 댓글