Programming/Spring Boot

DB 트래픽 분산을 위한 DataSource Read, Write 분기 처리

Jan92 2022. 2. 8. 23:28

'Spring Data JPA, Master - Slave 구조에 따른 Read, Write 분기 처리'

DataSource Read, Write 분기 처리는 많아지는 데이터베이스 요청에 따라 데이터베이스의 부하를  줄이기 위한 DB 이중화 구성 Master - Slave에서 많이 사용됩니다.

(Master - Slave 구조를 사용하는 가장 큰 이유는 Master에서는 쓰기, 수정, 삭제 요청을 처리하고 Slave에서는 읽기 요청만 처리하여 병목을 줄여주기 위함입니다.)

 

 

***

해당 포스팅은 MySQL Replication 및 Master -Slave 구조에 중점을 둔 것이 아니라 이 구조에서 Transaction ReadOnly 여부에 따라 DataSource를 나눠서 Connection을 가져오는 방법에 초점을 맞춘 내용입니다. 참고 부탁드리겠습니다.

 

 


 

 

'.properties 또는 .yml 파일'

# DataSource Master
spring.datasource.master.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.master.jdbc-url=jdbc:mariadb://localhost:3306/master_db
spring.datasource.master.username=root
spring.datasource.master.password=0000

# DataSource Slave
spring.datasource.slave.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.slave.jdbc-url=jdbc:mariadb://localhost:3306/slave_db
spring.datasource.slave.username=root
spring.datasource.slave.password=0000

먼저 하나의 데이터베이스에 연결하던 driverClassName, jdbc-url, username, password 부분을 master, slave 정보에 따라 나눕니다.

 

 


 

 

'DataSource 분기를 위한 RoutingDataSource'

public class RoutingDataSource extends AbstractRoutingDataSource {
  @Override
  protected Object determineCurrentLookupKey() {
    return (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) ? "slave" : "master";
  }
}

AbstractRoutingDataSource는 DataSource 동적 사용을 위한 핵심이 되는 추상 클래스입니다.

RoutingDataSource 클래스는 'AbstractRoutingDataSource의 determineCurrentLookupKey()' 메서드를 오버라이드 하며, 해당 메서드를 통해 사용할 DataSource를 분기 처리합니다.

여기서는 @Transactional(readOnly = true) 여부에 따라 master, slave DataSource를 나누는 기능을 수행하며, 나눠진 "master", "slave"는 DataSource를 나누는 key 값으로 작용됩니다.

 

 

***

determineCurrentLookupKey() 메서드는 로직에 따라 내부적인 구현이 바뀝니다.

(url 패턴에 따른 분기, Enum을 사용하여 넘어오는 enum 값에 따른 DataSource 선택, Session에서 Key 값을 통한 분기 등) 

 

 


 

 

'DataSourceConfiguration'

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = {"com.example.readwrite"})
public class DataSourceConfiguration {

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

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

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

    HashMap<Object, Object> dataSourceMap = new HashMap<>();
    dataSourceMap.put("master", masterDataSource);
    dataSourceMap.put("slave", slaveDataSource);
    routingDataSource.setTargetDataSources(dataSourceMap);
    routingDataSource.setDefaultTargetDataSource(masterDataSource);

    return routingDataSource;
  }

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

DataSource Bean을 생성하는 Configuration 클래스로 일반적인 DataSource가 아닌 RoutingDataSource를 생성해서 리턴하고 있습니다.

 

먼저 @ConfigurationProperties 어노테이션을 통해 .properties 파일에 master, slave로 나눈 값들을 가지고 와서 각각의 masterDataSource, slaveDataSource를 생성하고 Bean으로 등록합니다.

이어서 리턴되는 RoutingDataSource는 여러 개의(여기서는 master, slave) DataSource 객체를 Key, Value 형태로 담고 있고, determineCurrentLookupKey()라는 메서드에서 리턴하는 key값과 매칭 되는 DataSource 객체를 반환하게 됩니다.

그리고 RoutingDataSource는 최종적으로 LazyConnectionDataSourceProxy에 쌓여서 리턴되게 됩니다.

 

 

* @Qualifier 어노테이션의 기능

등록된 Bean에 하나의 타입만 있다면 해당 객체를 주입받지만, 위 경우처럼 masterDataSource, slaveDataSource 두 가지 DataSource 타입이 있는 경우 어떤 것을 사용할 것인지 정할 수 없는 문제가 발생합니다.

이러한 문제를 해결하기 위해서 @Qualifier 어노테이션을 사용하며, 같은 타입의 Bean이 등록됐을 때 찾으려는 이름을 정해준다고 생각할 수 있습니다.

 

 


 

 

'LazyConnectionDataSourceProxy'

스프링은 트랜잭션 시작 시 Connection의 실제 사용 여부와 무관하게 Connection을 확보합니다.

@Transactional
public void create(User user) {
    // do nothing
}

(이 코드에서도 실제 데이터베이스와 관련된 작업이 없지만 @Transactional 어노테이션 만으로 Connection을 잡아버립니다.)

 

이때 LazyConnectionDataSourceProxy를 사용하면 트랜잭션이 시작되더라도 실제로 Connection이 필요한 경우에만 DataSource에 Connection을 반환하는데요.

 

위 코드에서는 RoutingDataSource를 LazyConnectionDataSourceProxy로 감싸는 동작을 통해 Transaction 동기화 이전에는 Connection Proxy 객체를 획득하고, 실제 쿼리가 호출될 때 DataSource를 정하여 Connection을 획득할 수 있도록 작업됩니다.

(LazyConnectionDataSourceProxy로 감싸지 않으면 readOnly true 여부를 확인할 수 없게 됩니다.)

 

 

 

+++ 추가 내용 (2022.02.09)

종속성 순환 에러

작업 환경에 따라 위 코드로 분기처리를 했을 때 종속성 순환 에러가 발생하는 경우를 확인하였습니다.

 

  @Bean(name = "routingDataSource")
  @DependsOn({"masterDataSource", "slaveDataSource"})
  public DataSource routingDataSource(
      @Qualifier("masterDataSource") DataSource writeDataSource,
      @Qualifier("slaveDataSource") DataSource readDataSource) {
    ...
  }
  
  @Primary
  @Bean
  @DependsOn({"routingDataSource"})
  public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {
    ...
  }

종속성 순환 에러를 해결하는 첫 번째 방법은 @DependsOn 어노테이션을 통해 빈의 초기화 순서를 지정하는 것입니다.

 

 

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class Application {
   public static void main(String[] args) { ... }
}

두 번째 방법은 @SpringBootApplication 어노테이션에 DataSourceAutoConfiguration.class 비활성화(exclude)시키는 것입니다.

 

 

(해당 종속성 문제를 해결하기 위한 단순 방안만 먼저 찾아봤으며, 문제가 발생하는 자세한 원인과 해결 원리에 대해서는 자세하게 다시 한번 정리해서 포스팅하도록 하겠습니다.)

 

 

 

 

< 참고 자료 >

 

Spring Boot JPA - master slave 분기 처리 - transactional 방식 - mudchobo devlog

스프링 부트에서 JPA는 기본 셋팅은 1개의 datasource만 설정하게 되어 있다. 하지만, master / slave replication이 되어 있는 디비를 둘 다 연결하고 싶을 때에는 조금 까다롭게 설정해야 한다. 두 가지 방

mudchobo.github.io

 

[Spring-boot] Master - Slave 구조에 따른 Read, Write 분기

서론 데이터베이스를 이용한다면 대부분 쓰기보다 읽기 의 행위가 더 많습니다. DB의 부하를 줄이기 위해 다음과 같이 Master - Slave 구조를 많이 사용하는데요. 이러한 구조를 가지고 있을 때 Transe

k3068.tistory.com

 

 

의존성을 가진 다중 DataSource의 순환 참조 오류 분석

발단 내가 만든 프로젝트 중에는 에서는 2개 이상의 DataSource를 사용하고 있는 것이 있다. 의 Reader, Writer 엔드포인트에 연결되는 DataSource…

bgrooot.github.io