SW 개발/Spring

Spring Boot, JPA, 커넥션 풀을 read-write, read-only로 분리하기

지단로보트 2021. 12. 14. 23:00

개요

  • MySQL 데이터베이스의 Master-Slave 레플리케이션은, 조회만 발생하는 트랜잭션은 복수개의 Slave 노드로, 조회 뿐만 아니라 데이터베이스 변경점이 발생하는 트랜잭션은 1개의 Master 노드로 요청되어야 한다. 이번 글에서는 Spring Boot, JPA 환경에서 커넥션 풀을 read-write, read-only로 분리하는 방법을 설명하고자 한다.
  • 특히, Amazon Aurora MySQLSingle-Master 레플리케이션을 사용할 경우, 최대 15개의 레플리카 인스턴스를 운영할 수 있는데 애플리케이션 레벨에서 read-only 트랜잭션을 레플리카 엔드포인트를 바라보도록 설정하면 조회 성능을 크게 향상시킬 수 있다.

기본 흐름

  • 어노테이션에 따라 커넥션 풀을 분기해주는 클래스를 직접 제작하거나, 이미 공개된 것을 적용한다. 본 예제에서는 권남 님의 LazyReplicationConnectionDataSourceProxy 클래스를 사용하여 제작했다. [관련 링크]
  • @Transactional이 명시된 클래스 또는 메써드는 read-write 커넥션 풀을 사용하게 된다.
  • @Transactional(readOnly = true)이 명시된 클래스 또는 메써드는 read-only 커넥션 풀을 사용하게 된다.

application.yml

  • 환경 설정 프로필은 아래와 같이 작성한다. 최대한 Spring Boot의 기본 골격을 유지하는 방향으로 제작했다.
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url:
      read-write: {read-write-url}
      read-only: {read-only-url}
    username: {username}
    password: {password}
    hikari:
      maximum-pool-size: 30
      auto-commit: false
      connection-init-sql: SET NAMES utf8mb4

DatabaseConfiguration 클래스

  • 이제 DataSource 빈을 생성할 차례이다. read-write, read-only 2개의 DataSource 빈을 생성한 후, 앞단에서 프록시로 작동하는 DataSource 빈을 생성하는 것이 목적이다.
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration
@EnableJpaRepositories({"{your-repository-package}"})
@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
@EnableTransactionManagement
public class DatabaseConfiguration {

    @Value("${spring.datasource.driver-class-name}")
    private String DRIVER_CLASS_NAME;

    @Value("${spring.datasource.url.read-write}")
    private String READ_WRITE_URL;

    @Value("${spring.datasource.url.read-only}")
    private String READ_ONLY_URL;

    @Value("${spring.datasource.username}")
    private String USERNAME;

    @Value("${spring.datasource.password}")
    private String PASSWORD;

    @Value("${spring.datasource.hikari.maximum-pool-size}")
    private int MAXIMUM_POOL_SIZE;

    @Value("${spring.datasource.hikari.auto-commit}")
    private boolean AUTO_COMMIT;

    @Value("${spring.datasource.hikari.connection-init-sql}")
    private String CONNECTION_INIT_SQL;

    @Bean(name = "readWriteDataSource")
    public DataSource readWriteDataSource() {

        return this.buildDataSource(
            DRIVER_CLASS_NAME,
            READ_WRITE_URL,
            USERNAME,
            PASSWORD,
            "read-write",
            MAXIMUM_POOL_SIZE,
            AUTO_COMMIT,
            CONNECTION_INIT_SQL
        );
    }

    @Bean(name = "readOnlyDataSource")
    public DataSource readDataSource() {

        return this.buildDataSource(
            DRIVER_CLASS_NAME,
            READ_ONLY_URL,
            USERNAME,
            PASSWORD,
            "read-only",
            MAXIMUM_POOL_SIZE,
            AUTO_COMMIT,
            CONNECTION_INIT_SQL
        );
    }

    @Primary
    @Bean(name = "dataSource")
    public DataSource dataSource(
        @Qualifier("readWriteDataSource") DataSource readWriteDataSource, @Qualifier("readOnlyDataSource"
    ) DataSource readOnlyDataSource) {

        return new LazyReplicationConnectionDataSourceProxy(readWriteDataSource, readOnlyDataSource);
    }

    private DataSource buildDataSource(
        String driverClassName,
        String jdbcUrl,
        String username,
        String password,
        String poolName,
        int maximumPoolSize,
        boolean autoCommit,
        String connectionInitSql
    ) {
        HikariConfig config = new HikariConfig();
        config.setDriverClassName(driverClassName);
        config.setJdbcUrl(jdbcUrl);
        config.setUsername(username);
        config.setPassword(password);
        config.setPoolName(poolName);
        config.setMaximumPoolSize(maximumPoolSize);
        config.setAutoCommit(autoCommit);
        config.setConnectionInitSql(connectionInitSql);

        return new HikariDataSource(config);
    }
}

참고 글