Design E-Wallet
- 이체 기능 지원(다른 기능은 없다고 가정)
- 초당 트랜잭션 수(TPS): 1,000,000
- 재현성을 갖춘 시스템
- 가용성 99.99%
1,000,000 트랜잭션을 지원하기 위해, 필요한 데이터베이스 노드는 다음과 같이 계산할 수 있다.
- 하나의 데이터베이스 노드의 처리량: 1,000 TPS으로 가정
- 1,000,000 * 2(입금 + 출금) / 1,000 = 2,000 노드
API 단위 기능 정의
Section titled “API 단위 기능 정의”POST /wallet/transfer
Section titled “POST /wallet/transfer”이체를 수행하는 API, 요청 매개변수는 다음과 같다.
from_account_id: 출금 계좌 IDto_account_id: 입금 계좌 IDamount: 이체 금액currency: 통화 단위transaction_id: 트랜잭션 ID(중복 방지용)
인메모리 샤딩과 분산 트랜잭션
Section titled “인메모리 샤딩과 분산 트랜잭션”이체 기능을 구현하기 위해, 인메모리 샤딩을 사용하여 계좌 정보를 분산 저장한다. 이를 통해 데이터베이스의 부하를 줄이고, 빠른 조회 및 업데이트가 가능하다.
- 레디스와 같은 인메모리 데이터베이스를 사용하여 계좌 정보를 저장(
<사용자, 잔액>형태) - 1,000,000 TPS를 지원하기 위해, 클러스터를 구성하고 균등하게 샤딩
이 때 사용자의 계좌 정보가 다른 샤드에 저장될 수 있으므로, 이체 시 두 업데이트가 하나의 원자적 트랜잭션으로 처리되어야 한다.
2PC(2-Phase Commit)
Section titled “2PC(2-Phase Commit)”데이터베이스 자체에 의존하는 방법으로, 두 단계로 커밋을 진행한다.
- 락 획득 단계: 모든 관련 데이터베이스 노드에 트랜잭션을 준비 상태로 설정하고, 락을 획득
- 준비 단계: 락을 획득한 데이터베이스 노드들이 커밋이 가능한지 확인
- 커밋 단계: 모든 노드가 준비 상태라면 커밋을 수행, 그렇지 않으면 롤백
이 방법은 안전하게 트랜잭션을 처리할 수 있을 것 같지만, 다음과 같은 문제들이 존재한다.
- 성능 저하: 락을 획득하고 커밋을 처리하는 과정 자체에서 락 점유 시간이 길어져 성능이 저하됨
- SPoF: 락을 획득하고 커밋을 처리해주는 조정자에 장애가 발생하면, 락이 획득한 상태로 계속 유지됨
TCC(try-confirm-cancel)
Section titled “TCC(try-confirm-cancel)”TCC는 시도 -> 확정 / 취소로 구성된 트랜잭션 처리 방식이다.
- 조정자는 모든 데이터베이스에 트랜잭션에 필요한 자원 예약 요청(=Try)
- 조정자는 모든 데이터베이스로부터 회신을 받음
- 모두 성공 회신: 확정 요청(=Confirm)
- 하나 이상의 실패 회신: 취소 요청(=Cancel)
이 처리 방식은 2PC가 두 단계를 하나의 트랜잭션으로 처리한 것에 비해, 각 단계를 별도 트랜잭션으로 처리한다는 차이가 있다.
하지만 이 방식도 다음과 같은 문제점이 있다.
- 네트워크 지연이나 중간 단계 실패 시 복구 로직이 복잡해짐
- 비즈니스 로직에서 각 단계(Try, Confirm, Cancel)에 대한 별도 구현이 필요해 개발 부담이 증가
Saga 패턴
Section titled “Saga 패턴”Saga 패턴은 분산 트랜잭션을 처리하기 위한 패턴으로, MSA 환경에서는 표준으로 자리잡고 있다.
- 모든 연산은 순서대로 정렬되어, 각 연산은 자신의 데이터베이스에 독립 트랜잭션으로 실행
- 연산은 순서대로 실행되고, 각 연산이 성공하면 다음 연산이 실행
- 만약 연산이 실패하면, 이전 연산들을 취소하기 위해 역순으로 보상 트랜잭션을 통해 롤백
여기서 연산 수행 조율을 위한 방법은 다음 두 가지가 있다.
| Choreography | Orchestration | |
|---|---|---|
| 설명 | 각 서비스가 자신의 상태 변경을 이벤트로 발행하고, 다른 서비스가 해당 이벤트를 구독하여 처리 | 중앙 조정자가 모든 연산을 조율하고, 각 서비스에 명령을 전달 |
| 장점 | 서비스 간 결합도가 낮아 유연성 증가, 서비스 확장 용이 | 중앙 집중식 관리로 복잡한 로직 처리 용이 |
| 단점 | 서비스 간 의존성이 생길 수 있어 복잡성 증가 | 중앙 조정자가 SPoF가 될 수 있음 |
이벤트 소싱
Section titled “이벤트 소싱”전자 지갑 서비스와 같이 금융 거래를 다루는 시스템에서는 재현성(reproducibility)과 감사(audit) 가능성이 필수적이다.
- 특정 시점의 계정 잔액
- 과거 및 현재 계정 잔액이 정확성
이러한 요구사항을 충족하려면, 단순히 현재 상태만 저장하는 방식으로는 부족하며, 모든 상태 변화를 기록하고 필요 시 재구성할 수 있는 이벤트 소싱 방식을 사용할 수 있다.
이벤트 소싱 개념
Section titled “이벤트 소싱 개념”이벤트 소싱은 시스템의 상태를 이벤트로 기록하고, 이 이벤트들을 재생하여 현재 상태를 도출하는 방식으로, 다음과 같은 구성 요소로 이루어진다.
- Command(명령)
- 외부에서 전달된 의도가 명확한 요청
- 순서가 중요하므로 FIFO에 저장
- Event(이벤트)
- 명령에 의해 발생한 상태 변화의 기록
- 하나의 명령으로 여러 이벤트가 발생할 수 있음
- 순서가 중요하므로 FIFO에 저장
- State(상태)
- 이벤트를 재생하여 도출한 현재 상태
- 특정 시점으로의 복원이 가능
- State Machine(상태 기계)
- 이벤트 소싱 프로세스를 구동
- 명령의 유효성을 검사하고 이벤트 생성하여 상태를 변경
명령 -----> 상태 기계 ------> 이벤트 -----> 상태 기계 | | | | 읽기 --------> 상태 < ------ 적용지갑 서비스 예시
Section titled “지갑 서비스 예시”지갑 서비스의 경우 명령은 이체 요청이 될 것이고, 다음과 같이 동작할 수 있다.
- 사용자가 이체 요청
- 이체 요청(명령)은 FIFO 큐에 기록되고, 순서대로 지갑 서비스(상태 기계)에 전달
- 데이터베이스에 저장된 잔액(상태)를 조회
- 지갑 서비스(상태 기계)는 잔액이 충분한지 유효성 검사
- 잔액이 충분하다면, 이체(이벤트) 생성하여 FIFO 큐에 기록
- 이벤트는 상태 기계에 전달되어, 잔액을 업데이트하고 새로운 상태를 생성
이벤트 소싱이 다른 아키텍처에 비해 가장 중요한 장점은 재현성이다.
- 일반적인 상태 변경의 경우 최종 상태만 저장되며, 과거 상태를 복원하기 어려움
- 이벤트 소싱은 모든 상태 변화를 기록하므로, 특정 시점의 상태를 재구성할 수 있음
+------------------------------ Commands -------------------------------+| [ A -$1 -> C ] [ A -$1 -> B ] [ A -$1 -> D ] |+-----------------------------------------------------------------------+ | | |+------------------------------- Event Stream --------------------------+| [A:-$1] [C:+$1] [A:-$1] [B:+$1] [A:-$1] [D:+$1] |+-----------------------------------------------------------------------+ | | |+--------------------+ +--------------------+ +--------------------+| Snapshot t0 | | Snapshot t1 | | Snapshot t2 || A:5 B:4 C:3 D:2 | | A:4 B:4 C:4 D:2 | | A:3 B:5 C:4 D:2 |+--------------------+ +--------------------+ +--------------------+CQRS(명령-쿼리 책임 분리)
Section titled “CQRS(명령-쿼리 책임 분리)”명령(Command)과 질의(Query)를 서로 다른 처리 경로와 데이터 모델로 분리하는 아키텍처로, 이벤트 소싱을 사용할 때 자주 결합하여 사용한다.
- 쓰기 모델은 도메인 규칙과 트랜잭션 일관성에 맞게 처리
- 읽기 모델은 조회 성능과 쿼리 최적화에 맞춰 별도 구성
- 이벤트 소싱과 결합하면 쓰기 측에서 발생한 이벤트를 기반으로 읽기 모델(프로젝션)을 갱신
이 방식은 단순 DB 사본을 사용하는 방법과 비교할 때 다음과 같은 차이점이 있다.
| DB Replica | CQRS | |
|---|---|---|
| 목적 | 읽기 부하 분산 | 읽기/쓰기 모델 분리 |
| 스키마 | 원본 DB 스키마 복제 | 읽기 모델 최적화 가능 |
| 데이터 반영 시간 | 실시간 또는 지연 | 이벤트 기반 반영 |
고신뢰성 솔루션
Section titled “고신뢰성 솔루션”이벤트 소싱 아키텍처에서 래프트 노드 구조를 사용하여 SPOF 문제를 해결할 수 있다.
+------------------------------------------------------------+ | 팔로워 이벤트 -> 상태 기계 -> 상태 저장소 | +------------------------------------------------------------+ | 래프트 | +------------------------------------------------------------+ 요청 ----> | 리더 이벤트 -> 상태 기계 -> 상태 저장소 | +------------------------------------------------------------+ | 래프트 | +------------------------------------------------------------+ | 팔로워 이벤트 -> 상태 기계 -> 상태 저장소 | +------------------------------------------------------------+래프트 구조는 분산 시스템에서 일관성을 유지하기 위한 합의 알고리즘으로, 다음과 같은 특징이 있다.
- 각 노드는 같은 데이터를 가짐
- 리더 / 팔로워 구조로, 리더가 클라이언트 요청을 처리하고 팔로워는 리더의 로그를 복제
- 과반수 노드가 작동하는 한 시스템은 안정적으로 동작 가능
데이터 반영 지연 최소화
Section titled “데이터 반영 지연 최소화”CQRS 시스템에서는 쓰기 모델과 읽기 모델이 분리되어 있어 업데이트 시점을 정확히 알 수 없기 때문에, 폴링 방식으로 상태를 확인할 수 있지만 다음과 같은 문제점이 있다.
- 반영이 지연될 수 있으며, 언제 반영될지 알 수 없음
- 주기를 짧게 설정하면 성능 저하가 발생할 수 있음
이 문제는 리버스 프록시를 추가하여 개선할 수 있다.
+---------------------- 이벤트 수신 후 실행 상태를 역방향 프락시에 푸시 ------------------------------+ 응답 | | <---- | +---------------------------------------------------+ | 요청 ----> 리버스 프록시 ---> | 이벤트 -> 상태 기계 -> 이벤트 -> 상태 기계 -> 상태 저장소 | +--> 상태 기계 --> 상태 저장소 +---------------------------------------------------+ | | | +---------------------------- ------+- 요청을 리버스 프록시로 전달
- 리버스 프록시는 쓰기 모델에 이벤트를 전달
- 쓰기 모델에서 이벤트를 처리하고 읽기 전용 상태 기계에 이벤트를 전달
- 읽기 전용 상태 기계는 상태를 갱신하고, 리버스 프록시에 상태를 푸시
- 클라이언트는 리버스 프록시로부터 상태를 받아 응답