티스토리 뷰

이번글에서는 Spring Boot에서 Database Replication이 구성된 AWS RDS쪽으로 ReadWrite의 부하를 분산하게끔 구성을 해보려고 한다. 그전에 Database Replication에 대해 간단한 개념을 짚고 가자.

Database Replication

  • Replication의 사전적 의미는 복제다. 그렇다면 Database Replication은 데이터베이스를 복제하는걸 말하는데 기준이 되는 서버를 Primary라 불리우며 복제된 서버는 Secondary라고 부른다.
  • 기준이 되는 Primary는 1대로 구성되며 복제된 Secondary는 N대로 구성된다.

Database Replication 구성하는 이유

  • 트래픽이 급증할 경우 1대의 데이터베이스에 쓰기(insert, update, delete)읽기(select)를 모두 처리하게될 시 많은 부하가 발생되는데 이러한 부하를 분산하기위해 Primary에서는 쓰기Secondary에서는 읽기를 처리하게끔 구성한다.
  • Secondary를 N대로 구성하는 이유는 어플리케이션에서 데이터베이스에 액세스할 때 쓰기보단 읽기가 더 많이 발생되고 처리시간도 더 오래 걸릴 수 있기 때문이다.

Master와 Slave라는 표현은 일부러 사용하지 않았다.

 

실습환경

  • JAVA 17
  • Spring Boot 2.6.4
  • Gradle
  • AWS RDS Aurora(MySQL)

 

AWS RDS Aurora Cluster 생성하기

엔진을 Aurora로 선택하도록 하자.



클러스터이름과 마스터계정/암호를 입력하자.



테스트를 진행하기 때문에 default-vpc로 설정하고 퍼블릭 액세스를 허용하도록 하자.



마지막으로 추가 구성에서 맨 위를 보면 테스트로 연결할 데이터베이스 이름을 작성하고 맨 아래를 보면 삭제 방지 활성화 체크박스를 해제하도록 하고 데이터베이스를 생성하자.



약 10분정도 지나면 리전 클러스터, 라이터 인스턴스, 리더 인스턴스 모두가 사용가능 상태로 바뀐다. 리전 클러스터를 선택후 우측상단의 '작업 > 읽기 추가' 버튼을 클릭하자.



복제할 대상 인스턴스를 선택하고 퍼블릭 액세스 허용해주고 리더 인스턴스를 추가하자.



Spring Boot 프로젝트 생성

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'mysql:mysql-connector-java'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

build.gradle에 다음과 같은 디펜던시들을 추가해주자.

 

spring:
  datasource:
    replication:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: 마스터계정명
      password: 마스터계정비밀번호
      write:
        name: write
        url: jdbc:mysql://리전 클러스터(라이터 인스턴스)엔드포인트:3306/example
      reads:
        - name: read1
          url: jdbc:mysql://리전 클러스터(리더 인스턴스)엔드포인트:3306/example

application.yml파일의 구성내용이다. 리전 클러스터(리더 인스턴스) 엔드포인트를 적어두면 로드밸런싱을 해주지만 AWS RDS Aurora를 사용하지 않고 Read용 데이터베이스 엔드포인트를 여러개 적어야 할 상황이 있기 때문에 해당 프로퍼티는 List로 구성해두었다.자세한 내용은 하단 참고

 

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;

@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.replication")
public class ReplicationDataSourceProperties {
    private String username;
    private String password;
    private String driverClassName;
    private Write write;
    private List<Read> reads;

    @Getter
    @Setter
    public static class Write {
        private String name;
        private String url;
    }

    @Getter
    @Setter
    public static class Read {
        private String name;
        private String url;
    }
}

application.yml에 설정한 내용을 위 클래스와 매핑하도록 구성한다.

 

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.List;
import java.util.Map;

public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {
    private static final String READ = "read";
    private static final String WRITE = "write";
    private final ReadOnlyDataSourceCycle<String> readOnlyDataSourceCycle = new ReadOnlyDataSourceCycle<>();

    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        List<String> readOnlyDataSourceLookupKeys = targetDataSources.keySet()
                .stream()
                .map(String::valueOf)
                .filter(lookupKey -> lookupKey.contains(READ)).toList();
        readOnlyDataSourceCycle.setReadOnlyDataSourceLookupKeys(readOnlyDataSourceLookupKeys);
    }

    @Override
    public Object determineCurrentLookupKey() {
        return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
                ? readOnlyDataSourceCycle.getReadOnlyDataSourceLookupKey()
                : WRITE;
    }
}

Spring에서는 AbstractRoutingDataSource 라는 클래스를 제공해주는데 이 클래스는 DataSource와 각 DataSource를 조회할 key로 구성할 수 있게끔 해주는데 여기서 말하는 key는 Read와 Write 두개를 얘기한다.

 

그러면 Read용과 Write용 데이터베이스 커넥션을 언제 얻어야 할지 구분을 해야 하는데 그 구분은 @Transactional 어노테이션으로 구분이 가능하다.

 

@Transactional에 readOnly 속성이 true로 지정되면 Read 데이터베이스 커넥션을 얻고 false(기본값)면 Write 데이터베이스 커넥션을 얻는다.

 

import java.util.List;

public class ReadOnlyDataSourceCycle<T> {
    private List<T> readOnlyDataSourceLookupKeys;
    private int index = 0;

    public void setReadOnlyDataSourceLookupKeys(List<T> readOnlyDataSourceLookupKeys) {
        this.readOnlyDataSourceLookupKeys = readOnlyDataSourceLookupKeys;
    }

    public T getReadOnlyDataSourceLookupKey() {
        if (index + 1 >= readOnlyDataSourceLookupKeys.size()) {
            index = -1;
        }
        return readOnlyDataSourceLookupKeys.get(++index);
    }
}

해당 클래스는 AWS RDS Aurora를 사용하고 있다면 필요는 없지만 간략히 설명하자면 어플리케이션에서 N개의 Read용 데이터베이스에 로드밸런싱을 해준다고 생각하면 될 것 같다.자세한 내용은 하단 참고

 

import com.zaxxer.hikari.HikariDataSource;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Configuration
@RequiredArgsConstructor
public class ReplicationDataSourceConfiguration {
    private final ReplicationDataSourceProperties replicationDataSourceProperties;

    @Bean
    public DataSource routingDataSource() {
        ReplicationRoutingDataSource replicationRoutingDataSource = new ReplicationRoutingDataSource();

        ReplicationDataSourceProperties.Write write = replicationDataSourceProperties.getWrite();
        DataSource writeDataSource = createDataSource(write.getUrl());

        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(write.getName(), writeDataSource);

        List<ReplicationDataSourceProperties.Read> reads = replicationDataSourceProperties.getReads();
        for (ReplicationDataSourceProperties.Read read : reads) {
            dataSourceMap.put(read.getName(), createDataSource(read.getUrl()));
        }

        replicationRoutingDataSource.setDefaultTargetDataSource(writeDataSource);
        replicationRoutingDataSource.setTargetDataSources(dataSourceMap);
        replicationRoutingDataSource.afterPropertiesSet();
        
        return new LazyConnectionDataSourceProxy(replicationRoutingDataSource);
    }

    private DataSource createDataSource(String url) {
        HikariDataSource hikariDataSource = new HikariDataSource();
        hikariDataSource.setDriverClassName(replicationDataSourceProperties.getDriverClassName());
        hikariDataSource.setUsername(replicationDataSourceProperties.getUsername());
        hikariDataSource.setPassword(replicationDataSourceProperties.getPassword());
        hikariDataSource.setJdbcUrl(url);
        
        return hikariDataSource;
    }
}

application.yml에 정의된 데이터베이스 접속정보들을 읽어드려서 Write용 DataSource, Read용 DataSource를 생성 후 Spring에서 라우팅 해줄 수 있게끔 설정한다.LazyConnectionDataSourceProxy에 대한 설명은 여기서 정리하진 않음

여기까지 했는데 데이터베이스 Connection이 되지 않으면 아래를 확인하자

RDS 인스턴스의 보안그룹을 확인해보자.



인바운드 규칙의 '소스' 부분에 해당부분처럼 되어있으면 해당 보안그룹에 속해있는곳 에서만 요청을 보낼 수 있다.



해당 규칙을 삭제하고 위와 같이 생성하자.

 

Read / Write 테스트

@Transactional, @Transactional(readOnly = true) 두개의 메서드를 생성 후 데이터베이스 url값을 확인해보니 정상적으로 라우팅이 된 것 같다.

 

참고

리전 클러스터, 라이터 인스턴스, 리더 인스턴스를 각각 클릭해보면 리전 클러스터는 클러스터 라이터 인스턴스, 클러스터 리더 인스턴스의 엔드포인트가 존재하고 라이터 인스턴스, 리더 인스턴스는 각각 독립적인 엔드포인트가 존재한다.

 

리전 클러스터의 클러스터 리더 인스턴스의 엔드포인트는 리더 인스턴스들이 N개의 인스턴스로 구성되어 있을 때 로드밸런싱을 해줄 수 있는 엔드포인트 이다. 라이터 인스턴스, 리더 인스턴스의 독립적인 엔드포인트를 요청할 경우 로드밸런싱을 해주지 않는다.

 


    private final JpaProperties jpaProperties;

    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        EntityManagerFactoryBuilder entityManagerFactoryBuilder = createEntityManagerFactoryBuilder();
        return entityManagerFactoryBuilder.dataSource(tracingDataSource()).packages("com.example.*.entity").build();
    }

    private EntityManagerFactoryBuilder createEntityManagerFactoryBuilder() {
        AbstractJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        return new EntityManagerFactoryBuilder(vendorAdapter, jpaProperties.getProperties(), null);
    }

JPA를 사용하고 있다면 위 ReplicationDataSourceConfiguration.class 안에다 해당 코드를 넣어주자. 라우팅 데이터소스 구성 시 EntityManagerFactory를 JAVA 설정으로 잡아줘야 인식을 하는 것 같다.

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함