안녕하세요, 리브입니다!
성장한 서비스는 대량의 데이터를 핸들링 해야합니다.
대량의 데이터를 효율적으로 핸들링하기 위한 일환으로 읽기 DB 서버(Read Replica)와 쓰기 DB 서버를 분리해서 데이터베이스의 부하를 분산시킵니다.
우리는 @Transactional
어노테이션을 이용해 readOnly
가 true
로 설정되어 있으면 읽기 DB를 이용하고 그렇지 않다면 쓰기 DB를 이용하도록 설정하려고 합니다.
1
application.yml 수정두 DB 서버를 application.yml
에 정의했습니다. (당연히 기존 datasource는 제거 했습니다.)
spring:
datasource:
read:
jdbc-url: jdbc:mysql://localhost:3308/mydb # Read replica
username: root
password : rootpassword
driver-class-name: com.mysql.cj.jdbc.Driver
write:
jdbc-url: jdbc:mysql://localhost:3307/mydb # Write replica
username: root
password : rootpassword
driver-class-name: com.mysql.cj.jdbc.Driver
2
DataSourceRouter
클래스 생성우리는 분리된 DB를 운영 서버에서만 적용할 예정이기 때문에 @Profile
어노테이션을 이용해서 prod
프로파일에서만 활성화되도록 지정했습니다.
AbstractRoutingDataSource
는 런타임에 적절한 데이터 소스를 선택하는 기능을 제공하는 추상 클래스입니다. 이 추상 클래스를 상속 받으면 determineCurrentLookupKey
메서드를 오버라이드 할 수 있습니다. 이 메서드의 반환 값이 사용할 데이터 소스가 됩니다.
이 메서드를 통해 @Transactional
의 readOnly
가 true
이면 Read replica
를 사용하도록 설정합니다.
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.support.TransactionSynchronizationManager;
@Profile("prod")
public class DataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); // 현재 트랜잭션이 읽기 전용인지 확인
return readOnly ? "read" : "write";
}
}
3
DataSourceConfig
클래스 생성DataSourceConfig
를 통해 데이터 소스를 설정하고 관리합니다.
먼저 @EnableJpaRepositories(basePackages = {패키지 이름})
는 JPA Repository 빈을 활성화합니다. 별도로 basePackages
속성을 주지 않으면 @SpringBootApplication
에 설정한 빈 scan 범위와 동일한 범위로 빈을 scan합니다.
@ConfigurationProperties(prefix = "spring.datasource.read")
로 application.yml
에 spring.datasource.read
로 시작하는 설정을 가져와서 데이터 소스 속성을 자동으로 설정합니다.
readDataSource()
와 writeDataSource()
에서 생성된 DataSource
객체는 애플리케이션이 데이터베이스와 연결되도록 합니다.
routeDataSource()
메서드를 통해 읽기 전용 데이터 소스와 쓰기 전용 데이터 소스 중 작업에 따라 적절한 데이터 소스를 선택할 수 있도록 설정합니다.
dataSourceRouter.setDefaultTargetDataSource(writeDataSource)
설정으로 기본 데이터 소스를 쓰기 전용 데이터 소스로 설정합니다. 쓰기 전용 데이터베이스는 쓰기 작업 뿐 아니라 읽기 작업도 가능하지만 읽기 전용 데이터베이스는 쓰기가 불가능하기 때문에 쓰기 작업 중 예외가 발생하지 않도록 기본 값을 쓰기 전용 데이터 소스
로 설정했습니다.
dataSource()
메서드로 기본 데이터 소스를 설정합니다. 우리는 바로 위 메서드에서 반환된 DataSourceRouter
객체가 기본 데이터 소스로 설정됩니다. 즉, 읽기 전용, 쓰기 전용 데이터 소스를 관리하는 데이터가 우리 애플리케이션의 기본 데이터 소스 역할을 하게 됩니다.
LazyConnectionDataSourceProxy
로 데이터베이스 연결을 지연시켜, 실제로 데이터 소스에 접근할 때만 연결을 생성합니다. 리소스 사용을 최적화하고, 필요 없는 연결을 피할 수 있습니다.
import javax.sql.DataSource;
import java.util.Map;
import com.zaxxer.hikari.HikariDataSource;
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.context.annotation.Profile;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
@Configuration
@Profile("prod")
@EnableJpaRepositories(basePackages = "develup")
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.write")
public DataSource writeDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.read")
public DataSource readDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@DependsOn({"writeDataSource", "readDataSource"})
public DataSource routeDataSource() {
DataSourceRouter dataSourceRouter = new DataSourceRouter();
DataSource writeDataSource = writeDataSource();
DataSource readDataSource = readDataSource();
Map<Object, Object> dataSourceMap = Map.of(
"write", writeDataSource,
"read", readDataSource
);
dataSourceRouter.setTargetDataSources(dataSourceMap);
dataSourceRouter.setDefaultTargetDataSource(writeDataSource);
return dataSourceRouter;
}
@Bean
@Primary
@DependsOn("routeDataSource")
public DataSource dataSource() {
return new LazyConnectionDataSourceProxy(routeDataSource());
}
}