Post

Spring Boot와 함께 Database Replication 사용하기

Spring Boot와 함께 Database Replication 사용하기

앞서 정리한 Database Replication 사용하기(with MySQL) 글에서 살펴본 것처럼,
Spring Boot 환경에서도 동일하게 적용할 수 있습니다.

Master/SourceSlave/Replica의 명칭 관련해서는 설명의 편의를 위해 MasterReplica를 사용하겠습니다. (MySQL 8.0 이후로 Master/Slave -> Source/Replica 로 변경되었습니다.)

@Transactional을 이용해서 사용하기

Spring Boot에서는 @Transactional 을 통해 Master로 보낼지, Replica로 보낼지 정할 수 있습니다.

Spring Boot에서는 기본적으로 DB(DataSource)에 연결할지 결정하는 기능은 내장되어 있지 않기 때문에
readOnly = falseMaster DataSource
readOnly = trueReplica DataSource로 연결되는 설정이 필요합니다.

그럴려면 AbstractRoutingDataSource 를 상속 받아서 사용할 수 있게 해줘야 합니다.

AbstractRoutingDataSource 란?

AbstractRoutingDataSourcegetConnection()(DB 연결이 필요한 순간) 시점에
determineCurrentLookupKey()로 현재 트랜잭션 컨텍스트(예: readOnly)에서 라우팅 키를 결정하고,
그 키에 매핑된 DataSource로 위임하는 추상 클래스입니다.

이 메커니즘을 통해 Write는 Master로, 읽기는 Replica 로 보낼 수 있습니다.

테스트 진행

RoutingDataSource 생성

1
2
3
4
5
6
7
8
9
10
11
12
public enum RoutingKey {
  MASTER, REPLICA
}

public class RoutingDataSource extends AbstractRoutingDataSource {

  @Override
  protected Object determineCurrentLookupKey() {
    boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
    return readOnly ? RoutingKey.REPLICA : RoutingKey.MASTER;
  }
}

TransactionSynchronizationManager를 통해 트랜잭션의 ReadOnly 여부를 확인해서 Master 혹은 Replica로 보낼지 설정합니다.

DataSourceProps 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Setter
@Getter
public class DataSourceProps {
  private String driverClassName;
  private String jdbcUrl;
  private String username;
  private String password;
  private String poolName;
  private Integer maximumPoolSize;
  private Integer minimumIdle;
}

@ConfigurationProperties(prefix = "app.datasource.master")
@Getter
@Setter
public class MasterDataSourceProps extends DataSourceProps { }

@ConfigurationProperties(prefix = "app.datasource.replica")
@Getter
@Setter
public class ReplicaDataSourceProps extends DataSourceProps{ }

공통 DataSource 설정을 위해 DataSourceProps 선언하고, 이를 상속 받아서
MasterDataSourcePropsReplicaDataSourceProps을 선언합니다.

DataSourceConfig 설정

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
36
37
38
39
40
41
42
43
44
45
46
47
48
@Configuration
@EnableConfigurationProperties({ MasterDataSourceProps.class, ReplicaDataSourceProps.class })
public class DataSourceConfig {

  @Bean
  public HikariDataSource masterDataSource(MasterDataSourceProps p) {
    HikariDataSource ds = new HikariDataSource();
    ds.setDriverClassName(p.getDriverClassName());
    ds.setJdbcUrl(p.getJdbcUrl());
    ds.setUsername(p.getUsername());
    ds.setPassword(p.getPassword());
    return ds;
  }


  @Bean
  public HikariDataSource replicaDataSource(ReplicaDataSourceProps p) {
    HikariDataSource ds = new HikariDataSource();
    ds.setDriverClassName(p.getDriverClassName());
    ds.setJdbcUrl(p.getJdbcUrl());
    ds.setUsername(p.getUsername());
    ds.setPassword(p.getPassword());
    return ds;
  }


  @Bean
  public DataSource routingDataSource(HikariDataSource masterDataSource,
                                      HikariDataSource replicaDataSource) {
    RoutingDataSource rds = new RoutingDataSource();

    Map<Object, Object> targets = new HashMap<>();
    targets.put(RoutingKey.MASTER, masterDataSource);
    targets.put(RoutingKey.REPLICA, replicaDataSource);

    rds.setTargetDataSources(targets);
    rds.setDefaultTargetDataSource(masterDataSource);
    rds.afterPropertiesSet();
    return rds;
  }


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

RoutingDataSource이랑 DataSourceConfig 설정을 끝냈다면,
application.properties 설정도 진행합니다.

application.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# JPA 설정
spring.jpa.open-in-view=false
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=debug

# Master DataSource
app.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
app.datasource.master.jdbc-url=jdbc:mysql://localhost:3307/testdb?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Seoul
app.datasource.master.username=root
app.datasource.master.password=rootpass
app.datasource.master.pool-name=MasterPool
app.datasource.master.maximum-pool-size=10
app.datasource.master.minimum-idle=1

# Replica DataSource
app.datasource.replica.driver-class-name=com.mysql.cj.jdbc.Driver
app.datasource.replica.jdbc-url=jdbc:mysql://localhost:3308/testdb?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Seoul
app.datasource.replica.username=root
app.datasource.replica.password=rootpass
app.datasource.replica.pool-name=ReplicaPool
app.datasource.replica.maximum-pool-size=10
app.datasource.replica.minimum-idle=1

위에처럼 설정을 끝내고 실행하면 다음과 같은 결과를 Console 창에서 확인할 수 있습니다.

1
2
3
4
5
6
7
Database JDBC URL [Connecting through datasource 'org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy@13803a94']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown

이렇게 undefined/unknown로 나오는 현상은 LazyConnectionDataSourceProxy를 사용했기 때문에
부팅 시점에는 하이버네이트가 프록시만 보게 되고, getConnection()이 호출되기 전까지 실제 커넥션이 생성되지 않기 때문입니다.

자, 이제 설정이 끝났으니 실제 서비스로 사용을 시작할게요.

서비스 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
@RequiredArgsConstructor
public class UserService {
  private final UserRepository userRepository;

  // WRITE -> MASTER
  @Transactional
  public void insert(String name) {
    UserEntity user = new UserEntity();
    user.setName(name);
    userRepository.save(user);
  }

  // READ -> REPLICA
  @Transactional(readOnly = true)
  public String find(String name) {
    return userRepository.findByName(name).getName();
  }
}

위에처럼 @Transactional 을 사용할 때, readOnly를 통해 MASTER, REPLICA로 보낼 수 있습니다.

This post is licensed under CC BY 4.0 by the author.