Home [soldout] DB Replication 적용
Post
Cancel

[soldout] DB Replication 적용

# 문제점


DB에 접근하는 로직은 크기 쓰기에 대한 요청과 읽기에 대한 요청이 많습니다.

그렇기 때문에 @Transactional 어노테이션을 선언하는 경우에도 읽기만을 위한 메소드엔 @Transactional(readOnly = true)로 선언해 의도치 않은 데이터 수정에 대해 방지하는 구조로 설계합니다.

또한 DB의 입장에선 쓰기에 대한 요청보단 읽기에 대한 요청이 더 자주 발생하는 경우가 많습니다. 이런 상황에서 읽기 처리에 대한 datasource와 쓰기 처리에 대한 datasource가 공유된다면 상대적으로 처리량은 적고 소요시간은 긴 쓰기 요청에 대한 처리가 비효율적으로 처리될 가능성이 높아집니다.

예를 들어 새로운 제품에 대한 등록을 하기 위해 날린 요청을 처리하기 위해 수많은 조회 요청에 대한 처리를 기다려야 할 수 있다는 뜻입니다.

이를 조금 더 효율적인 구조로 변경하기 위해 Master-Slave 구조를 적용해 보기로 했습니다.

# 해결방안


Master-Slave 구조

Master-Slave 구조란 위 그림처럼 단순 읽기 처리에 대한 요청과 쓰기 처리에 대한 요청을 처리해주는 DB를 구분해 목적이 다른 두 요청의 처리 속도를 높혀줄 수 있는 구조를 의미합니다.

이를 구성하기 위해선 Master를 위한 datasource와 Slave를 위한 datasource를 구분해 빈 객체로 생성해줄 필요가 있습니다.

MasterDataSourceConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Configuration
@MapperScan(value = " api.soldout.io.soldout.mapper",
            sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MasterDataSourceConfig {

  @Value("${mybatis.mapper-locations}")
  String mapperPath;

  @Primary
  @Bean(name = "masterDataSource")
  @ConfigurationProperties(prefix = "spring.datasource.master.hikari")
  public DataSource masterDataSource() {

    return DataSourceBuilder.create()
        .type(HikariDataSource.class)
        .build();
  }

  @Primary
  @Bean(name = "masterSqlSessionFactory")
  public SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource,
      ApplicationContext applicationContext) throws Exception {
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dataSource);
    sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(mapperPath));
    return sqlSessionFactoryBean.getObject();
  }

  @Bean(name = "masterSessionTemplate")
  public SqlSessionTemplate sqlSessionTemplate(@Qualifier("masterSqlSessionFactory")
      SqlSessionFactory firstSqlSessionFactory) {
    return new SqlSessionTemplate(firstSqlSessionFactory);
  }

}

Master DB에 대한 DataSource 구성 파일입니다.

SlaveDataSourceConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Configuration
@MapperScan(value = " api.soldout.io.soldout.mapper",
            sqlSessionFactoryRef = "slaveSqlSessionFactory")
public class SlaveDataSourceConfig {

  @Value("${mybatis.mapper-locations}")
  String mapperPath;

  @Bean(name = "slaveDataSource")
  @ConfigurationProperties(prefix = "spring.datasource.slave.hikari")
  public DataSource slaveDataSource() {

    return DataSourceBuilder.create()
        .type(HikariDataSource.class)
        .build();
  }

  @Bean(name = "slaveSqlSessionFactory")
  public SqlSessionFactory sqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource,
      ApplicationContext applicationContext)
      throws Exception {
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dataSource);
    sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(mapperPath));
    return sqlSessionFactoryBean.getObject();
  }

  @Bean(name = "slaveSessionTemplate")
  public SqlSessionTemplate sqlSessionTemplate(@Qualifier("slaveSqlSessionFactory")
      SqlSessionFactory firstSqlSessionFactory) {
    return new SqlSessionTemplate(firstSqlSessionFactory);
  }

}

Slave DB에 대한 DataSource 구성 파일입니다.

ReplicationRoutingDataSource

1
2
3
4
5
6
7
8
9
10
11
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {

  @Override
  protected Object determineCurrentLookupKey() {

    return TransactionSynchronizationManager
        .isCurrentTransactionReadOnly() ? DataSourceType.SLAVE : DataSourceType.MASTER;

  }

}

AbstractRoutingDataSource 는 조회된 key를 기반으로 등록된 Datasource 중 하나를 호출하도록 조건문을 선언해줬습니다.

  • @Transactional(readOnly = false) : Master DB 연결
  • @Transactional(readOnly = true) : Slave DB 연결

RoutingDataSourceConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class RoutingDataSourceConfig {

  @Bean(name = "routingDataSource")
  public DataSource routingDataSource(
            @Qualifier("masterDataSource") final DataSource masterDataSource,
            @Qualifier("slaveDataSource") final DataSource slaveDataSource) {

    ReplicationRoutingDataSource routingDataSource = new ReplicationRoutingDataSource();

    Map<Object, Object> dataSourceMap = new HashMap<>();

    dataSourceMap.put(DataSourceType.MASTER, masterDataSource);
    dataSourceMap.put(DataSourceType.SLAVE, slaveDataSource);

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

    return routingDataSource;

  }

  @Bean(name = "dataSource")
  public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {

    return new LazyConnectionDataSourceProxy(routingDataSource);

  }

}

생성한 두 datasource 객체를 Map 자료구조에 저장하고 ReplicationRoutingDataSource에 저장했습니다.

키로 활용되는 값은 Enum 클래스로 추가 선언해 문자열에 의한 에러를 최대한 방지해줬습니다.

# 마치며


Replication 구조를 DB에 적용함으로써 하나의 서버로만 구성하는 구조에서 서버 다운 시 발생할 수 있는 심각한 장애를 방지하는 구조로 리팩토링할 수 있었습니다.

# 참고 자료


  • https://k3068.tistory.com/102
  • https://taes-k.github.io/2020/03/11/sprinig-master-slave-dynamic-routing-datasource/
This post is licensed under CC BY 4.0 by the author.

[soldout] 즉시 구매 기능 구현시 책임 분리 문제

[soldout] 내부 생성 객체에 대한 테스트 방법 구성