[시스템 디자인] 12. 전자 지갑
[시스템 디자인] 12. 전자 지갑
시스템 디자인 시리즈의 글입니다.
전자지갑은 유저가 지갑에 돈을 넣어놓고 필요할 때 사용. 다른 사용자 지갑으로 직접 송금을 지원한다. 전자지갑 어플리케이션의 백엔드를 설계해보자.
0. 내 설계
- 일단 DB는 무조건 RDB를 써야할듯. (결제시스템 신뢰기반, 트랜잭션기반 → 실패, 장애시 롤백이 되어야함)
API
- GET /api/v1/balance : 잔액 조회
- POST /api/v1/deposite : 돈을 입금함
- POST /api/v1/withdraw : 돈을 출금함
- 순서가 중요하니까 동기식으로 개발하지않을까
- 부하분산 및 retry를 위해 큐 도입할수있을듯
1. 문제 이해
- 전자 지갑 간 이체
- 1M TPS
- 99.99% 안정성
- 일반 은행의 기준보다 빡빡하다
- 트랜잭션
- 재현성
개략적 추정
- tps 거론 = transaction 기반 DB 사용
- tps별 DB 벤치마크 결과
- 일반 RDB : 1K TPS 지원한다 가정, 1M TPS위해 1K DB 필요
- 이체명령 = 입금, 출금 연산 = 2K DB 필요
2. 개략적 설계
API 설계
- POST /v1/wallet/balance_transfer
- 한 지갑에서 다른 지갑으로 자금 이체
- amount는 string 타입 추천
인메모리 샤딩
- 지갑 어플리케이션은 모든 사용자 계정의 잔액을 유지해야함
- <사용자, 잔액> 관계의 key-value 저장소가 good
- redis는 좋은 선택 중 하나.
- 클러스터 구성 및 균등 파티셔닝(샤딩) 필요
- 파티션 주소를 저장하거나, zookeeper를 사용해 관리
- 지갑서비스 : 이체 명령 처리를 담당
- 이체 명령 수신
- 이체 명령 유효성 검증 (validation)
- 명령 유효 → 두 계정의 잔액 갱신
- 무상태 서비스 ⇒ 수평적 확장 용이
분산 트랜잭션
- 데이터베이스 샤딩
- 서로 다른 DB노드를 갱신하려는 연산을 atomic하게(=원자적으로) 수행해야함
- redis보다 RDB 노드 사용이 good
- 서로다른 노드를 업데이트해야하는데 이 작업이 정확히 동시에 처리된다는 보장은 여전히 X
- 2단계 커밋 (2PC)
방법
- 조정자가 DB에 읽기 및 쓰기 작업 수행
- 대상 DB 모두에 락이 걸림
- 대상 DB 모두에게 트랜잭션 준비 요청
- 조정자는 DB의 응답을 받아 다음 절차 수행
- 모든 DB가 ‘yes’ 응답시 트랜잭션 커밋 요청
- 하나라도 ‘no’ 응답시 모든 DB에 트랜잭션 중단 요청
- 저수준방안. (데이터베이스 자체에 의존하는 방안)
- 모든 DB가 X/Open XA 표준 만족
- 다른 노드의 메세지 기다리는동안 락이 계속 유지됨 → 성능 저하
- 조정자가 SPOF가 됨
- 두 단계가 하나의 transaction
- TC/C (Try-Confirm/Cancel)
방법
- 조정자가 모든 DB에 트랜잭션을 위한 자원 예약 요청
- 모든 DB로부터 회신을 받음
- 모두 ‘yes’ 으답시 DB에 작업확인 요청 = Try-Confirm
- 하나라도 ‘no’ 응답시 모든 DB에 취소 요청 = Try-Cancel
- 고수준 방안. (undo 절차를 비즈니스로직으로 어플리케이션이 구현)
- 각 단계가 별도 transaction
- 이미 종료된 기존 트랜잭션의 효과를 되돌리기 위해, 또다른 트랜잭션을 수행함.
- 특정 DB 종류에 구애받지 않음
- 어플리케이션 계층의 비즈니스로직에서 세부사항을 관리하고 분산 트랜잭션의 복잡성을 처리해야함
- 단계별 상태 테이블
- 지갑서비스 장애시 과거 작업기록을 복구하려면?
- 각 단계 상태정보를 트랜잭션 데이터베이스에 저장
- 불균형 상태
- 분산 트랜잭션 실행 도중에는 항상 데이터 불일치 발생.
- 저수준방안 → 우리가 신경쓸필요없음
- 고수준방안 → 우리가 직접 처리
- 잘못된 순서 실행
- 단계별 상태테이블에 취소명령에대한 플래그를 저장
- 시도명려잉 도착했을때 먼저 도착한 취소명령이 있다면 실패를 반환
- 병렬 수행 가능 ⇒ latency에 민감하다면 추천.
- SAGA
방법
- 모든 연산은 순서대로 정렬. 각 연산은 독립 트랜잭션으로 실행
- 연산은 첫번째부터 마지막까지 순서대로 실행.
- 연산실패시 전체 프로세스는 실패 연산부터 역순으로 보상트랜잭션을 통해 롤백
- MSA 에서는 사실상 표준
- 연산 실행 순서 조율
- 분산조율 : MSA에서 모든 서비스가 다른 서비슫의 이벤트를 구독하여 작업 수행. 탈중앙화 방식. 비동기식 통신. 내부적으로 state machine 유지.
- 중앙 집중형 조율: 하나의 조정자가 모든 서비스가 올바른 순서로 작업 실행하도록 조율. 일반적으로 선호됨. 복잡한 상황을 잘 처리
- 병렬수행 불가능.
이벤트 소싱
- 배경
- 감사를 받을 수 잇음.
- 감사 질문 (특정시점의 잔액, 잔액의 정확성, 변경후 로직 검증)에 대해 체계적으로 답하기 위한 설계 철학 중 하나가 이벤트소싱
- 정의
- 명령
- 외부에서 전달된 의도가 명확한 요청
- 순서가 중요하여 FIFO 큐에 저장
- 무작위성이나 I/O가 포함될 수 잇음.
- 유효하지 않을 수 잇음
- 이벤트
- 명령 이행 결과
- 검증된 사실 (실행이 끝난 상태)
- deterministic(결정론적)임.
- 하나의 명령으로 여러개의 이벤트가 만들어 질 수 잇음.
- 같은 명령에 항상 동일한 이벤트가 생성되는것은 아님.
- 명령 순서를 따라야하므로 FIFO 큐에 저장
- 상태
- 이벤트가 적용될 때 변경되는 내용
- 상태 기계
- 시스템이 상태를 가지고잇으면 상태를 특정 규칙에 따라 transistion(전환)함
- 명령의 유효성을 검사
- 이벤트를 생성
- 이벤트를 적용하여 상태 갱신
- deterministic하게 동작해야함
- 명령
- 재현성
- DB는 특정 시점의 잔액이 얼마인지만 나타냄
- 이벤트를 처음부터 다시 재생하면 과거 잔액 상태를 언제든 재구성 가능
- 재현성을 갖추면 감사에 쉽게 답이 가능해짐.
CQRS (명령-질의 책임 분리)

- 클라이언트는 여전히 계정의 잔액을알 수 없음
- 상태이력 DB의 읽기전용 사본을 생성해 외부와 공유하면 가능해짐
- 상태기록 담당하는 상태기계 1개
- 읽기전용 상태기계 여러개 → state view를 만들고 query(질의)함
- readonly state machine은 다양한 상태 표현을 도출
- 조금 뒤쳐질수는 있으나 결과적 일관성을 보장
3. 상세 설계
높은 성능, 안정성, 확장성을 달성하기 위한 기술들을 알아보자
고성능 이벤트 소싱

- 파일기반의 명령 및 이벤트 목록
- 네트워크 전송시간을 줄일 수 있음
- 로컬디스크에 추가연산만 하는것은 순차적 쓰기 연산으로 매우 빠름. HDD에서도 잘 동작
- 최근 명령과 이벤트를 메모리에 캐시 → mmap 활용
- 파일기반 상태
- 잔액정보를 파일기반 RDB에 저장 (SQLite, RocksDB)
- 스냅샷
- 과거 특정 시점의 상태를 스냅샷으로 저장
- 큰 binary 파일이며 일반적으로 HDFS에 저장
신뢰할 수 있는 고성능 이벤트 소싱
- 신뢰성 분석
- 데이터 내구성이 보장만 되면 언제든 복구 가능.
- 상태,. 스냅샷 = 이벤트목록 재생시 언제든 복구 가능
- 명령어 = 명령에서 이벤트를 생성하는 과정은 deterministic하지않음.
- 이벤트 자체는 historical fact이며 immutable함
- 이벤트 = 높은 신뢰성을 보장할 유일한 데이터
- 합의
- 이벤트 목록을 여러 노드에 복제해야 높은 안정성 제공 가능
- 데이터 손실이 없어야함
- 로그 파일 내 데이터는 모든 노드에 동일하게 있어야함
- 합의기반 복제 방안이 적합
- 합의기간 알고리즘(Raft Algoritm)으로 가정
- 정족수를 만족하면 전체 시스템은 정상으로 판단하고 정상동작
- 하나의 노드만 리더, 나머지는 팔로워가됨
- 고신뢰성 솔루션
- SPOF 문제 해결
- 이벤트 소싱은 동일한 이벤트 목록에서 항상 동일한 상태가 만들어지게함
분산 이벤트 소싱
- 전자지갑 업데이트는 빨라야하는데 CQRS 는 흐름이 느림
- 데이터 샤딩 및 분산 트랜잭션 구현이 필요
풀 vs 푸시 CQRS
- 풀모델
- 외부 사용자와 이벤트 소싱 노드 사이에 reverse proxy 추가
- reverse proxy를 놨다면 읽기전용 상태 기계를 수정하여 응답속도 높이기 가능
- 읽기전용 상태 기계가 이벤트 수신시 바로 reverse proxy에 푸시하도록함
- 클라이언트는 주기적으로 실행상태를 질의해 조회함
- 푸시모델
- 읽기 전용 상태 기계가 이벤트를 수신하자마자 실행상태를 reverse proxy에 푸시하도록함
- 사용자에게 실시간으로 응답이 이뤄지는 느낌을 줄수있음
분산 트랜잭션

- 모든 이벤트 소싱 노드 그룹이 동기적 실행 모델을 채택하면 TC/C or SAGA 솔루션을 재사용할 수 있음
- 각 파티션마다 해당 솔루션으로 구성됨