
보편적으로 DB 쓰기와 읽기 작업의 비율을 20:80로 판단하며, 서비스가 확장될수록 읽기 전용 DB를 활용하는 것이 필수적이다.
@Transactional(readOnly=true) 어노테이션 사용 시 읽기 전용 db 인스턴스를 통해 db 데이터를 불러오도록 한다.
아래와 같은 효과를 얻을 수 있다.
⇒ write에 몰리는 트래픽을 읽기전용 db로 분산 가능
⇒ 향후 db 스케일 아웃이 필요할 때 읽기전용 db인스턴스만 추가해도 소스 수정 없이 인프라 작업만으로 대응 가능
write 1개 / read를 2개 인스턴스를 구성하고 스프링의 @Transactional 어노테이션의 readOnly 속성에 따라 데이터소스 분기하는 코드 구현
r/w replica는 기본적으로 한개의 write instance와 n개의 read instace들을 구성할 수 있다. 읽기작업만 필요한 서비스의 경우 read replica를 이용하고 이 레플리카의 instance들 중 어떤 것을 선정해서 호출할 지 결정해야하는데, 운영 환경 구성에 따라 이 로직을 직접 구현해야할 수 도 있다.
이미지 출처: https://linux.systemv.pe.kr/2019/02/aws-aurora-endpoint-and-was-connection-pool/spring:
datasource:
write:
driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
jdbc-url: jdbc:log4jdbc:postgresql://write-postgres.ap-northeast-2.rds.amazonaws.com:5432/postgres
username: user
password: qwer1234
maximum-pool-size: 20
read:
driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
jdbc-url: jdbc:log4jdbc:postgresql://read-postgres.ap-northeast-2.rds.amazonaws.com/postgres
username: user
password: qwer1234
maximum-pool-size: 10
AbstractRoutingDataSource를 상속받아서 DynamicRoutingDataSource 클래스로 재사용하도록 개발함determineCurrentLookupKey() 메소드 재구현 - DB 읽기전용 여부에 따라 미리 구성해놓은 DynamicRoutingDataSource targetDatasources map에서 어떤 datasource를 사용할지 메소드만 overrideimport org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import static org.springframework.transaction.support.TransactionSynchronizationManager.isCurrentTransactionReadOnly;
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return isCurrentTransactionReadOnly() ? "read" : "write";
}
}
DynamicRoutingDataSource 인스턴스 세팅LazyConnectionDataSourceProxyimport com.zaxxer.hikari.HikariDataSource;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
@RequiredArgsConstructor
public class DataSourceConfig {
@ConfigurationProperties(prefix = "spring.datasource.write")
@Bean
public DataSource writeDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@ConfigurationProperties(prefix = "spring.datasource.read")
@Bean
public DataSource readDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@DependsOn({"writeDataSource", "readDataSource"})
public DataSource routingDataSource(@Qualifier("writeDataSource") DataSource writeDataSource,
@Qualifier("readDataSource") DataSource readDataSource) {
DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("write", writeDataSource);
dataSourceMap.put("read", readDataSource);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(writeDataSource);
return routingDataSource;
}
@Bean
public PlatformTransactionManager transactionManager(
@Qualifier(value = "dataSource") DataSource lazyRoutingDataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(lazyRoutingDataSource);
return transactionManager;
}
@Primary
@DependsOn("routingDataSource")
@Bean
public DataSource dataSource(DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
}
Spring은 Transaction에 접근하는 순간 DataSource의 커넥션을 가져온다. 현재 필요한 것은 사용할 db를 결정하는 시간을 readOnly 속성에 따라 나중에 결정하도록 해야하므로 데이터소스가 달라져야 하므로 LazyConnectionDataSourceProxy를 사용한다.
참고: https://sup2is.github.io/2021/07/08/lazy-connection-datasource-proxy.html
spring batch에 read/write 분리 사용하기 - https://jojoldu.tistory.com/506
웹서비스 확장 전략 - https://www.slideshare.net/charsyam2/webservice-scaling-for-newbie