Spring

Spring 에서 Master Slave Datasource 분리하기

developer.woozi 2023. 3. 14. 03:05

개요

데이터베이스에서 고가용성을 위한 작업으로 Replication 을 선택하는 것은 거의 당연한일이다.
이외에도 Replication 을 가지면 얻을 수 있는 특징은 아래와 같다.

  • High Availability
  • Scalability
  • DisasterRecovery
  • Backup
  • Analysis
  • DataWareHouse

 

Application 의 요청을 처리하는 DB 인스턴스가 하나일 경우
모든 쓰기 요청과 읽기 요청 대해서 작업을 처리해야하는 문제가 발생한다.
당연스럽게도 대용량의 요청이 들어오면 이를 처리하지 못해 지연 및 장애가 발생하게 된다.
(Scale Up 으로 해결할 수 있으나, 더 많은 대용량 요청이 들어오면 한계가 드러난다.)  

 

사실 단일 DB 인스턴스 뿐만 아니라 분산 환경에서도 문제가 발생할 수 있다.
여러 서버 그리고 여러 DB 가 다대다 관계로 존재한다고 가정한다.
모든 쓰기 요청과 읽기 요청이 중첩되서 호출될 것이고 진입점
즉, 호출하는 서버가 하나 더 늘어나게 되면, 더 큰 부하를 야기하게 될 것이다.

이 같은 문제를 Replication 과 CQRS 그리고 Query Offloading 을 통해 해결할 수 있다.

CQRS(Command and Query Responsibility Segregation)는 명령 및 쿼리 책임 분리로
쓰기와 읽기를 분리하는 것으로 실제 우리가 수행하는 작업에 따라 바라보는 대상을 달리하는 것이고
Query Offloading 은 읽기 분배라는 뜻으로 읽기 수행에 있어서 여러 노드로 분산을 한다는 뜻이다.

정리하자면 아래와 같다.

  • Master에게는 데이터 동시성이 높게 요구되는 트랜잭션을 담당한다.
  • Slave에게는 데이터 동시성이 꼭 보장될 필요는 없는 읽기 전용 트랜잭션을 담당한다.(분산 가능)

 

CQRS 를 애플리케이션 구현 패턴(읽기/쓰기 서비스 분리)로 볼 수 있겠지만
여기서는 개념만 따왔다는 점만 양해 부탁드리고 
그리고 꼭 위 같은 방법으로만 해결할 수 있는 것은 아니라는 것도 유념 바란다.(cluster, sharding 등등)


시스템 아키텍처

예제 프로젝트를 구성하는 스킬셋은 아래와 같다.

  • Spring Boot, MVC, JPA
  • MariaDB & Replication, HAProxy
  • Docker, Docker-Compose

예제 아키텍처는 다음과 같다.

시스템 아키텍처

 

전체 소스 코드는 다음 링크에서 확인이 가능하다.

 

GitHub - the-developer-lab/the-server-lab: the-server-lab

the-server-lab. Contribute to the-developer-lab/the-server-lab development by creating an account on GitHub.

github.com

 

GitHub - the-developer-lab/the-server-lab: the-server-lab

the-server-lab. Contribute to the-developer-lab/the-server-lab development by creating an account on GitHub.

github.com


구현하기

DataSource 란?

JDBC DriverManager, HikariCP, DBCP 와 같은 구현체의 추상화로
Java 에서 다양한 라이브러리로부터 Database 커넥션을 활용할 수 있도록 만든 인터페이스.

인프런, 스프링 DB 1편 - 데이터 접근 핵심 원리

 

huisam 님의 블로그를 빌어 Datasource 는 아래와 같이 정리가 가능하기도 합니다.

  1. DB와의 연결을 위한 Factory의 역할로, Connection을 맺어주는 역할이다
  2. Connection 객체를 생성하면 관리는 Connection Manager에게 위임한다
  3. Transaction 관리자와 함께 활용되어 Transaction들을 처리한다

DataSource 를 통해 우리는 다양한 데이터베이스 커넥션 구현체를 호출하여 사용할 수 있다.
즉, DataSource 구현체를 구현하고 이를 활용하도록 해야 우리가 원하는 DB 에 접근할 수 있다.


DataSource 연결

DataSource 를 이용해서 2개 이상의 인스턴스를 연결하는 방법은 크게 2가지가 있다.

  • RW / RO 별로 Datasource를 만들어 개발자가 선언하여 처리하는 방법
  • Datasources에서 트랜잭션의 분기처리를 Lazy 하게 처리하는 방법

 

RW / RO 별로 Datasource를 만들어 개발자가 선언하여 처리

전체 소스 코드는 다음 링크에 있다.

간단하다고 생각할 수 있지만, 서로 다른 Datasource 를 가지는 도메인이 많으면 많을수록 복잡해진다.

application.yml

  • spring.datasrource.master 로 master 데이터 소스 정보를 입력 받는다.
  • spring.datasrource.slave 로 slave 데이터 소스 정보를 입력 받는다.
  • jdbc-url 와 같은 네이밍은 DatasourceBuilder 를 이용하기 위함이다.

 

ReadWriteDatasourceConfiguration

  • application.yml 로 부터 입력받은 프로퍼티 값을
    DataSourceBuilder 를 통해서 입력받아 master/slave Datasource 를 반환한다.
  • @ConfigurationProperties 에 대해서 찾아보는 것도 추천한다.

 

UserDataSourceConfiguration.kt

  • JPA 나 Jooq 와 같은 ORM 을 사용한다면 각각 필요한 빈들을 설정해주어야 한다.
  • Read/Write 에 대한 EntityManagerFactory 를 만들어주어야하므로 별도 빈으로 생성했다.(RW/RO)
  • Read/Write 에 대한 트랜잭션처리가 다르므로, 별도의 TransactionManager 들을 만들었다.(RW/RO)
  • User 뿐만 아니라 Board 와 같은 각각의 도메인별 Datasource 마다 처리해주어야 한다.

 

UserUsecase

  • Facade 서비스인 userUsecase 를 호출하여 각 비즈니스 로직을 처리하고 있다.
  • 확실한 트랜잭션 분리를 위해 Read/Write 서비스를 분리하여 적용시켰다.

 

UserWriteService

  • 서비스 레벨에서 Transactional 을 달아주었다.
  • 여러 transactionManager 빈들이 올라가있으므로 명시적으로 사용할 빈을 지정한다.
  • 여기서는 userReadWriteTransactionManger 이다.

 

UserReadService

  • UserReadService 도 위와 같은 형태로 개발을 해놓았다.

 

해당 로직들을 기반으로 실행하면 아래와 같은 결과가 나타난다.



해석하자면 아래와 같다.

  • Master-1-RW : Insert 구문이 동작한다.
  • Slave-1-RO : Insert 구문(replication) + Select Count 구문
  • Slave-2-RO : Insert 구문(replication)

 

단일 데이터 소스에서 트랜잭션의 분기처리를 Lazy 하게 처리

 

RoutingDataSource 처리 프로세스
스프링에서는 다양한 DataSource 맺고 이를 로직으로 Routing 처리할 수 있도록 지원하고 있다.
AbstractRoutingDataSource 추상 클래스를 구현하고 Bean으로 등록하면 자연스럽게 활용할 수 있다.

동작 방식은 아래와 같다.

  1. determineCurrentLookupKey()를 통해 LookupKey를 도출한다.
  2. LookupKey에 해당하는 dataSource가 Map에 있는지 확인하고 가져온다
  3. 존재하지 않는다면, resolvedDefaultDataSource()를 통해 defaultDataSource를 사용한다.
  4. defaultDataSource 조차도 없다면 IllegalStateException 예와가 발생한다

 

이제 동작 방식에 대해서 간단히 살펴보았으니 실제 구현을 진행해보자.

DataSourceRouter

 

RoutingDatasourceConfiguration

  • Master 와 Slave 를 기준으로 Datasource 를 생성한다.
  • 두 Datasource 를 기반으로, DataSourceRouter 를 정의하고 빈으로 등록한다.
  • DataSourceRouter 를 내려받아 LazyConnectionDataSourceProxy 를 빈으로 등록한다.

 

LazyConnectionDataSourceProxy 는
실제 Connection 을 사용해야할 시점(트랜잭션 시점)에 Lazy 하게 조회해서 가져온다.
참고로 이렇게 빈 등록을 2개로 나눈 것은 AbstractRoutingDataSource 초기화 시점 때문이다.

AbstractRoutingDataSource 동작 방식

  • AbstractRoutingDataSource 은 InitializingBean 을 구현하고 있다.
  • afterPropertiesSet 을 살펴보면, targetDatasource 를 그제서야 resolvedDataSources 에 등록한다.
  • 빈을 최초 생성하기 이전에, getConnection 을 호출하면 resolvedDataSources 가 아직 세팅되지 않았기에 에러가 발생한다.

 

LazyConnectionDataSourceProxy 동작 방식

  • 정리하자면, LazyConnectionDataSourceProxy 은 생성자로 주입받은 Datasource 로부터 getConnection() 을 호출한다.
  • 즉, 앞서 언급한대로 AbstractRoutingDataSource 의 생성 시점 이전이므로 에러가 발생한다.
  • 참고로 최초 커넥션을 맺는 이유는, AutoCommit 과 Isolation 설정을 초기화하기 위해서다.

 

다시 본론으로 들어와서 위와 같은 환경 구성을 완료한다면
별도의 EntityManger 또는 TransactionManager 를 구성하지 않아도
@Transactional 의 readOnly 필드에 따라서 Datasource 를 달리 가져갈 수 있다.

참고

 

[JPA] DataSource 를 연결하는 방법 & RoutingDataSource 설정

Java & kotlin 기반으로 Spring 을 개발하시는 분들은 너무나 익숙한 그림일텐데요 오늘은 Application과 JPA 단에 대해서 깊이 알아보는 시간보다는 DataSource를 통해서 어떻게 DataBase와 통신하는지에 대해

huisam.tistory.com

 

스프링에서 편리하게 Master / Slave 분기 처리하기

도입 실제 서비스를 운영하다 보면 데이터베이스가 여러 개의 노드로 분산되어 Master / Slave (또는 Multi Master) 구조로 이루어져 있는 경우가 많습니다. 이때 어플리케이션 레벨에서 어떻게 DataSource

sightstudio.tistory.com

 

DB Replication 설정하기 · Issue #399 · woowacourse-teams/2021-gpu-is-mine

이슈 설명 DB 가용성 증대와 단일 장애점 해소를 위한 Replication 도입

github.com