데이터베이스에서 replication 은 데이터베이스간의 데이터 복제를 의미한다. 하나의 데이터베이스에서 읽기와 쓰기 작업을 동시에 진행할 경우 병목 현상이 발생할 확률이 높다. 이를 방지하기 위해 흔히 Master(Write) 와 Slave(Read) 로 나누어, Master 에서 변경된 데이터를 Slave 로 복제하는 것을 replication 이라고 한다.
Java 어플리케이션에서 분기 처리하는 방법은 커넥션 풀(DataSource) 을 Master 와 Slave 로 나누어 주고, JDBC 의 Connection.setReadOnly 메소드나 Spring 의 @Transactional 를 사용하는 것이다.
Spring Boot 에서의 read / write 분기 처리
1. DB 설정 (application.yml)
spring:
datasource:
master:
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://dev.cluster-abc.rds.amazonaws.com:3306/kps
read-only: false
username: username
password: password
slave:
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://dev.cluster-ro-abc.rds.amazonaws.com:3306/kps
read-only: true
username: username
password: password
2. DataSource Bean 등록
@Configuration
public class DataSourceConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master.hikari")
public DataSource masterDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave.hikari")
public DataSource slaveDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
}
3. AbstractRoutingDataSource 구현
: Read-Write 가 동시에 필요하거나 복제 지연으로의 이슈를 막기 위해 AbstractRoutingDataSource 구현.
public class routingDataSourceImpl extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) ? "slave" : "master";
}
}
4. Lazy 설정
: LazyConnectionDataSourceProxy 설정으로 실제 쿼리 직전에 connection 을 생성하여 불필요한 커넥션 점유 방지.
@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {
@Primary
@Bean
public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
@Bean
public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
RoutingDataSourceImpl routingDataSourceImpl = new RoutingDataSourceImpl();
Map<Object, Object> targetDataSource = new HashMap<>();
targetDataSource.put("master", masterDataSource);
targetDataSource.put("slave", slaveDataSource);
routingDataSourceImpl.setTargetDataSources(targetDataSource);
routingDataSourceImpl.setDefaultTargetDataSource(masterDataSource);
return routingDataSourceImpl;
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master.hikari")
public DataSource masterDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave.hikari")
public DataSource slaveDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
}
5. @Transactional 명시
@Service
public class ExampleService {
...
@Transactional(readOnly = true)
public List<ExampleEntity> getList() {
return exampleRepository.getList();
}
@Transactional // default : readOnly = false
public Integer insertExample() {
return exampleRepository.insertExample();
}
}
6. 테스트
위의 코드를 예로 들면 updateExample() 를 실행하여 데이터 삽입을 확인하고,
getList() 를 실행했을 때 slave dataSource 가 구성되며 삽입한 데이터가 출력되면 성공.
...
HikariPool-1 - configuration:
HikariPool-1 - Starting...
...
HikariPool-2 - configuration:
HikariPool-2 - Starting...
...
WRITTEN BY
- 손가락귀신
정신 못차리면, 벌 받는다.