Springboot Application과 RDS, Aurora DB

짱구·2024년 1월 4일
3
post-thumbnail

안녕하세요.
이번 주제는 RDS와 Aurora DB를 레플리케이션까지 설정하고 Spring Boot 애플리케이션에서 연결연결 및 설정입니다.
SpringBoot Application에서 Transaction의 readOnly 여부로 데이터베이스 포워딩 하는 작업까지 진행해보겠습니다.

VPC

가장 먼저 해야할 일은 VPC 설정입니다.

VPC를 생성해줍시다.

보안 그룹

postgresql의 5432 포트를 열어주겠습니다.

RDS

AWS Console에 접속하신뒤 RDS 메뉴로 갑니다.

생성

표준생성, PostgreSQL을 선택하겠습니다.

단일 DB 인스턴스를 생성해도 괜찮지만 저희는 Aurora DB와 비교를 해야하기 때문에 다중 AZ DB 인스턴스를 선택해주시면 됩니다.

데이터베이스 설정은 식별자, 마스터 사용자, 암호를 입력해주세요.

저희는 테스트용이기 때문에 아래와 같이 가장 가격이 저렴한 데이터베이스를 생성할게요~

아까 만들었던 vpc, 보안그룹을 등록하고 퍼블릭 엑세스를 허용해야합니다.

인증방식을 골라준뒤 RDS를 생성해줍니다.

Read DB 생성

ReadDB도 동일하게 방금 만들어준 데이터베이스를 선택하고 작업 탭에서 읽기 전용 복제본을 생성해줍니다.

테스트겸 읽기 전용 데이터베이스기 때문에 단일 DB 인스턴스로 만듭니다.

위에서 만들때랑 동일하게 설정해야합니다.

connection

생성이 된걸 확인했다면 접속을 해보겠습니다.

masterDB인 rds-test에 들어가서 endpoint를 복사합니다.

복사를 했다면 IntelliJ에 들어가서 접속합니다.

  1. host에는 복사한 endpoint를 입력
  2. user에는 RDS를 등록할 때 만들었던 username을 입력
  3. password도 RDS를 등록할 때 만들었던 password를 입력
  4. test connection을 시도하고 성공했다면 OK 버튼 클릭

Aurora DB

AuroraDB를 사용하여 MasterDB 한개와 ReadDB 다중화를 생성하겠습니다.

AuroraDB는 일반 RDS와는 다르게 MasterDB와 ReadDB 간의 핸들링을 간편하게 하기 위해 MasterDB와 ReadDB 각각에 대한 엔드포인트를 제공하는 대신, MasterDB용 Load Balancer와 ReadDB용 Load Balancer 두 개의 엔드포인트만을 제공합니다.

이를 통해 사용자는 두 가지 Load Balancer 엔드포인트만 관리하면 되므로 관리가 편리해집니다.

생성

위에서 만든 RDS와 거의 동일한 스펙으로 생성합니다.

RDS를 만들때와 동일하게 연결을 구성합니다.

나머지 추가구성은 Default로 하고 생성합니다.

connection

정상적으로 생성이 되었다면 아래와 같이 WriterDB, ReaderDB가 생성이 되어야합니다.
그리고 아래에 존재하는 엔드포인트를 복사해서 접속을 하시면 성공입니다.

이제 SlaveDB(ReaderDB)를 여러개 만들고 ReaderDB Cluster Endpoint로만 요청을 보내면 내부적으로 로드밸런싱을 해서 트래픽을 분산해줍니다.

정상적으로 connection이 되었다면 스프링에서 추가적인 설정을 해보겠습니다.

Spring

application yml

설정 파일에서는 MasterDB와 SlaveDB에 Cluster Endpoint를 입력해주시면 됩니다.

spring:
  application:
    name: testest

  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        use_sql_comments: true

  datasource:
    master:
      hikari:
        jdbc-url: jdbc:postgresql://rds-test-db.cluster-your-rds-endpoint.ap-northeast-2.rds.amazonaws.com:5432/example_db
        username: postgres
        password: your-password
        driver-class-name: org.postgresql.Driver

    slave:
      hikari:
        jdbc-url: jdbc:postgresql://rds-test-db.cluster-ro-your-rds-endpoint.ap-northeast-2.rds.amazonaws.com:5432/example_db
        username: postgres
        password: your-password
        driver-class-name: org.postgresql.Driver

Configure

@Transaction 설정입니다.

@Transaction이 ReadOnly 일 경우 SlaveDB를 바라보게 하고 @Transaction이 ReadOnly가 아닐 경우는 MasterDB를 바라보게 설정했습니다.

private const val MASTER_DATASOURCE = "masterDataSource"
private const val SLAVE_DATASOURCE = "slaveDataSource"
private const val SLAVE = "slave"
private const val MASTER = "master"

@Configuration
class TransactionConfig {

    @Bean(name = [MASTER_DATASOURCE])
    @ConfigurationProperties(prefix = "spring.datasource.master.hikari")
    fun masterDataSource(): HikariDataSource =
        DataSourceBuilder.create()
            .type(HikariDataSource::class.java)
            .build()

    @Bean(name = [SLAVE_DATASOURCE])
    @ConfigurationProperties("spring.datasource.slave.hikari")
    fun slaveDataSource(): HikariDataSource =
        DataSourceBuilder.create()
            .type(HikariDataSource::class.java)
            .build()
            .apply { this.isReadOnly = true }

    @Bean
    @DependsOn(MASTER_DATASOURCE, SLAVE_DATASOURCE)
    fun routingDataSource(
        @Qualifier(MASTER_DATASOURCE) masterDataSource: DataSource,
        @Qualifier(SLAVE_DATASOURCE) slaveDataSource: DataSource,
    ): DataSource {
        val dataSources = hashMapOf<Any, Any>().apply {
            this[MASTER] = masterDataSource
            this[SLAVE] = slaveDataSource
        }

        return RoutingDataSource().apply {
            setTargetDataSources(dataSources)
            setDefaultTargetDataSource(masterDataSource)
        }
    }

    @Bean
    @Primary
    @DependsOn("routingDataSource")
    fun dataSource(routingDataSource: DataSource) =
        LazyConnectionDataSourceProxy(routingDataSource)
}

class RoutingDataSource : AbstractRoutingDataSource() {
    override fun determineCurrentLookupKey(): Any =
        when {
            TransactionSynchronizationManager.isCurrentTransactionReadOnly() -> SLAVE
            else -> MASTER
        }
}

성능

DB가 2개의 엔드포인트로 나뉘어져 있지만 JPA의 1차 캐시는 같이 공유되기 때문에 ReadDB에서 조회 한 뒤 WriteDB에서 재조회 했을 때 WriteDB에는 select query를 날리지 않습니다.

Example

@Service
@Transactional
class PersistentCacheService(
    private val persistentReadOnlyService: PersistentReadOnlyService,
    private val persistentWriteOnlyService: PersistentWriteOnlyService,
) {
    fun updateEntity(id: Long, value: String) {
        persistentReadOnlyService.getEntityById(id)
        persistentWriteOnlyService.updateEntity(id, value)
    }
}

@Component
@Transactional(readOnly = true)
class PersistentReadOnlyService(private val testRepository: TestRepository) {
    fun getEntityById(id: Long) =
        testRepository.findByIdOrNull(id)
            ?: throw IllegalArgumentException("id not found")
}

@Component
@Transactional(readOnly = false)
class PersistentWriteOnlyService(private val testRepository: TestRepository) {
    fun updateEntity(id: Long, value: String) {
        testRepository.findByIdOrNull(id)?.apply {
            this.name = value
        }
    }
}

PersistentCacheService의 updateEntity method를 실행했을 때 나오는 Query입니다.

select te1_0.id, te1_0.name from test_entity te1_0 where te1_0.id=?
    
update test_entity set name=? where id=?

조회가 2번 발생하지 않고 1번만 발생하는 것도 확인할 수 있습니다!

가격비교

백업용 DB를 제외한 금액표입니다.

끝!

profile
코드를 거의 아트의 경지로 끌어올려서 내가 코드고 코드가 나인 물아일체의 경지

0개의 댓글