안녕하세요, 리브입니다!

성장한 서비스는 대량의 데이터를 핸들링 해야합니다.
대량의 데이터를 효율적으로 핸들링하기 위한 일환으로 읽기 DB 서버(Read Replica)와 쓰기 DB 서버를 분리해서 데이터베이스의 부하를 분산시킵니다.

우리는 @Transactional 어노테이션을 이용해 readOnlytrue로 설정되어 있으면 읽기 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 메서드를 오버라이드 할 수 있습니다. 이 메서드의 반환 값이 사용할 데이터 소스가 됩니다.
이 메서드를 통해 @TransactionalreadOnlytrue이면 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.ymlspring.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());
    }
}
profile
팀 데벨업입니다

0개의 댓글

관련 채용 정보