하나의 mysql db만 운영하면 모든 crud작업을 혼자 감당해야합니다.
그래서 보통 db의 읽기 작업인 select문의 요청이 많은 것을 감안해서
읽기 전용 복제본을 만드는 것입니다.
복제본은 여러개를 만들수도 있으나 본 글에서는 하나만 만들도록 하겠습니다.
Multi AZ ( 다중 availability zone )이란 쉽게말해서 백업 DB를 만들어서
백업 DB가 실제 쓰이지는 않지만 본 DB의 내용을 동기화 하고 있다고 생각 하시면 됩니다.
RDS를 사용중이라 콘솔에서 간단하게 생성할 것입니다.
백업과 자동승격에 대한 내용은 본 글에서는 다루지 않겠습니다만...
이 부분은 그냥 master인스턴스에 multi AZ 을 적용하면 자동으로 백업을 해두고
master에 이상이 생기면 백업(스탠바이)이 자동으로 master를 대체합니다.
즉 읽기 전용 replica가 master로 승격되는 것은 아닙니다.
서론이 길었습니다. 본격적으로 한 단계씩 해보겠습니다.
RDS에서 이미 생성된 본 DB인스턴스를 선택한 후 작업-읽기 전용 복제본 생성을 눌러줍니다.
인스턴스 식별자 : 그냥 아무 이름이나 설정해 줍니다.
인스턴스 구성 : 버스터블 클래스 , db.t3.micro를 선택했습니다.
가용성 : 다중 AZ DB 인스턴스. 위에서 설명한 것 처럼 replica도 백업을 만들어 놓는 것입니다.
네트워크 : 퍼블릭 액세스 가능을 눌러줍니다.
데이터베이스 인증 : master DB에서도 암호인증을 했기 때문에 똑같이 암호인증으로 합니다.
설정은 원하시는 대로 맞춰서 하시면 됩니다.
이렇게 만들기만 해서는 실제 적용이 되지 않습니다.
프로젝트에 적용하기 위해선 DataSourceConfig, RoutingDataSource 클래스를 작성해주어야합니다.
대부분의 코드가 다 비슷하지만 저는 가장 간단한 방법을 사용했습니다.
@Slf4j
@Configuration
public class DataSourceConfiguration {
private static final String MASTER_SERVER = "MASTER";
private static final String REPLICA_SERVER = "REPLICA";
@Bean
@Qualifier(MASTER_SERVER)
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create()
.build();
}
@Bean
@Qualifier(REPLICA_SERVER)
@ConfigurationProperties(prefix = "spring.datasource.replica")
public DataSource replicaDataSource() {
return DataSourceBuilder.create()
.build();
}
@Bean
public DataSource routingDataSource(
@Qualifier(MASTER_SERVER) DataSource masterDataSource,
@Qualifier(REPLICA_SERVER) DataSource replicaDataSource
) {
RoutingDataSource routingDataSource = new RoutingDataSource();
HashMap<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("master", masterDataSource);
dataSourceMap.put("replica", replicaDataSource);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
@Bean
@Primary
public DataSource dataSource() {
DataSource determinedDataSource = routingDataSource(masterDataSource(), replicaDataSource());
return new LazyConnectionDataSourceProxy(determinedDataSource);
}
}
master와 replica의 DateSource를 각각 빈으로 등록해 줍니다.
@Qualifier 같은 타입의 빈을 등록할 때 구분을 해주기위해 적어줍니다.
@ConfigurationProperties(prefix = "spring.datasource.master")
이 부분은 properties 혹은 yml에 있는 db 설정를 읽어오기 위해 적어줍니다.
spring.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.master.jdbc-url=jdbc:mysql://master DB host 주소:포트/db이름
spring.datasource.master.username=유저네임
spring.datasource.master.password=비밀번호
spring.datasource.replica.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.replica.jdbc-url=jdbc:mysql://replica DB host 주소:포트/db이름
spring.datasource.replica.username=유저네임
spring.datasource.replica.password=비밀번호
(저는 이 properties의 정보를 계속 읽어오지 못해서 2일동안 온갖 삽질을 했었습니다..
원인은 spring.datasource.master.jdbc-url 여기서 jdbc-url을 그냥 url로 한게 원인이었습니다.
스프링부트의 자동구성이 읽지 못하는게 원인이라는데 참 어렵네요...)
RoutingDataSource객체를 새로 생성하는 걸 볼 수 있는데요. 어떤 쿼리가 오면 master로 갈지 replica로 갈지 결정해 주는 것입니다.
이 클래스는 아래에서 다시 설명하겠습니다.
@Primary가 붙은 DataSource를 주 데이터 소스로 취급합니다.
LazyConnectionDataSourceProxy : DB연결은 비용이 많이 드는 작업입니다. 그래서 실제로 쿼리를 실행하기 전까지 연결을 늦추는 것입니다. 이 부분은 사실 지금 우리가 하고있는 routing과는 상관없이 성능 향상을 위해서 적은 코드입니다.
@Slf4j
public class RoutingDataSource extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
String lookupKey = TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? "replica" : "master";
log.info("Current DataSource is {}", lookupKey);
return lookupKey;
}
}
위에서 생성해준 RoutingDataSource 을 정의하는 클래스입니다. AbstractRoutingDataSource를 추상화 하고 있습니다.
isCurrentTransactionReadOnly 이 메서드로 트랜젝션이 readOnly=true일 때는
replica를 반환합니다.
실제 replica를 쓸지 master를 쓸지 결정하는 로직은 AbstractRoutingDataSource에 구현되어있습니다.
lookupKey 값은 AbstractRoutingDataSource에 내장된 로직에 의해 setTargetDataSources에서 설정한 dataSourceMap을 조회할 때 사용됩니다.
앞서 dataSourceMap에 replica와 master를 넣었었죠??
( DataSourceFactory 패턴을 사용하면 readOnly를 꼭 달아야 하는 것은 아니지만
저는 이 방법을 쓰지 않았기 때문에 읽기전용 메서드에 모두 어노테이션을 달아주었습니다;; ㅜ_ㅜ)
@Transactional(readOnly = true)
public List<UserInfoDto> getToUsers(Long userId) {
이런식으로 어노테이션을 달아준 뒤 요청을 보내고 로그를 확인해보면.
replica로 잘 가고 있는 것을 볼 수 있습니다.
마찬가지로 쓰기 작업을 하면 master로 조회를 합니다.