돈을 잃지 않는 결제 시스템을 어떻게 만들까
출처: Dev.to
10 000 TPS 지불 시스템을 어떻게 구축할까
실제 엔지니어링 문제를 잡고, 먼저 일상 언어로 reasoning을 전개한 뒤, 마지막에 그 reasoning에 해당하는 정확한 용어를 붙입니다. 전문 용어는 마지막에 등장합니다.
문제
목표: 초당 10 000건의 트랜잭션을 처리하면서 단 한 건도 놓치지 않는 결제 시스템을 만든다.
1. 동시성 – “수야” 비유
비유: 두 손님이 동시에 수야 가게에 와서 마지막 꼬치를 원한다. 판매자는 손이 두 개뿐이라 동시에 서빙할 수 없다. 같은 꼬치를 두 사람에게 건네면 한 사람은 빈손이 된다.
결제 시스템에서도 두 개의 출금 요청이 정확히 같은 순간에 같은 지갑을 건드리면 같은 일이 일어난다:
- 두 요청이 잔액을 읽는다.
- 두 요청이 충분한 금액이 있다고 판단한다.
- 두 요청이 차감 기록을 쓴다.
결과 → 이중 지급.
용어: 동시성 문제 – 특히 레이스 컨디션.
2. 락킹 – “먼저 온 사람이 지갑을 락한다”
해결책: 첫 번째 요청이 데이터베이스의 지갑 행에 락을 잡는다. 두 번째 요청은 대기하고, 락이 해제된 뒤에 잔액을 다시 읽는다.
용어: 비관적 락킹 (또는 간단히 락킹).
3. 수직 확장 – “그릴은 더 크게, 요리사는 그대로”
비유: 판매자가 그릴을 업그레이드하고, 숯을 더 넣고, 더 빠른 스토브를 장착한다. 주문 처리 속도는 빨라지지만, 요리사는 여전히 한 명이므로 손님은 기다려야 한다.
용어: 수직 확장 – 단일 머신에 CPU, RAM, 혹은 더 빠른 스토리지를 추가하는 것. 한계치는 올리지만 근본적인 병목은 사라지지 않는다.
4. 수평 확장 – “가게를 여러 개, 각 가게마다 요리사 한 명씩”
비유: 판매자가 두 번째, 세 번째 수야 가게를 연다. 각 가게마다 자체(작은) 그릴이 있지만, 전체 출력량은 크게 늘어난다.
용어: 수평 확장 – 부하를 나누기 위해 머신(또는 인스턴스)을 추가하는 것. 이제 제한 요소는 하드웨어가 아니라 비용이 된다.
5. 읽기 복제본 – “‘내 주문 준비됐나요?’를 전담하는 직원”
비유: 대부분의 손님은 주문이 준비됐는지만 알고 싶어한다. 판매자는 주문 목록 복사본을 이용해 상태 문의만 담당하는 직원을 고용한다. 메인 요리사는 이제 요리만 하면 된다.
용어: 읽기 복제본 – 기본 데이터베이스가 쓰기를 담당하고, 복제본이 읽기 트래픽을 처리해 쓰기를 방해하지 않게 한다.
주의점: 복제본은 쓰기 용량을 늘리지 않는다. 초당 10 000건의 새로운 주문이 들어오면, 기본 DB가 여전히 한계에 부딪힌다.
6. 샤딩 – “여러 가게가 부하를 나눠 담당”
비유: 고객 1‑2 000은 가게 1에, 2 001‑4 000은 가게 2에 배정한다 등, 새로운 주문을 여러 위치에 분산한다.
용어: 샤딩 – 데이터를(또는 트래픽을) 여러 독립적인 데이터베이스/서버에 나누어 저장하는 것.
7. 분산 트랜잭션 – “SAGA 패턴”
문제: 가게 1이 돈을 차감하고, 가게 3에 주문을 해제하도록 요청한다. 전화가 끊기면 가게 3은 메시지를 못 받고 → 돈은 차감됐지만 주문이 전달되지 않는다.
단순한 해결책: 2단계 커밋(양쪽이 동의해야 진행) → 작동하지만 느리고 깨지기 쉽다.
더 나은 해결책: 가게 1이 “돈을 차감했으며, 가게 3이 해제를 해야 한다”고 기록한다. 가게 3이 확인하지 않으면 가게 1이 자동으로 환불한다. 각 서비스는 자체 단계를 수행하고, 보상 작업을 미리 준비한다.
용어: Saga – 보상 단계가 명시된 분산 트랜잭션을 관리하는 패턴.
8. 로드 밸런싱 – “입구 안내원”
비유: 다섯 개의 가게가 열려 있지만 손님들은 어느 줄이 가장 짧은지 모른다. 안내원이 모든 가게를 살피고 새 손님을 다음으로 비어 있는 가게에 안내한다.
용어: 로드 밸런서 – 서버 앞에 위치해 들어오는 트래픽을 분산시켜 특정 서버가 과부하되지 않게 한다(예: Nginx, HAProxy, Envoy).
9. 고가용성 & 장애 조치 – “예비 안내원”
문제: 안내원 한 명이 단일 장애 지점이 된다.
해결책: 기본 안내원이 실패하면 자동으로 인계받는 핫 스탠바이 예비 안내원을 배치한다.
용어: 장애 조치(Failover) – 중복 컴포넌트로 자동 전환하는 것, 이를 통해 고가용성(HA) 구성을 만든다.
10. 속도 제한 – “고객당 주문 제한”
비유: 한 사람이 1분에 할 수 있는 주문 수를 제한한다. 정상 트래픽은 크게 방해되지 않지만, 악의적인 사용자가 시스템을 압도하는 것을 막는다.
용어: 속도 제한(Rate limiting) – 클라이언트, API 키, IP 등별 요청 속도를 상한선으로 제한한다.
11. 대규모 가시성 – “어디부터 살펴봐야 할까?”
초당 10 000 TPS에서는 실패한 트랜잭션이 어디서든 발생할 수 있다:
- 입구(로드 밸런서)
- 주문 접수 서버(애플리케이션 서버)
- 결제 처리 서비스
- 기록 저장소(데이터베이스)
- 알림 전송 시스템(메시징)
로그, 메트릭, 트레이스가 수십 대의 머신에 흩어져 있다.
해결책:
- 구조화된 로깅 – 요청 ID가 포함된 JSON 로그.
- 중앙화 로그 집계 – ELK/EFK 스택, Loki, Splunk 등.
- 분산 트레이싱 – OpenTelemetry, Jaeger, Zipkin.
- 메트릭 & 대시보드 – Prometheus + Grafana.
- 알림 – PagerDuty / Opsgenie 로 지연, 오류율, 자원 포화 감시.
이렇게 하면 문제가 발생했을 때 한 곳에서 원인 파악을 시작할 수 있다.
12. 전체 구성
| 레이어 | 기법 | 왜 중요한가 |
|---|---|---|
| 동시성 제어 | 비관적 락킹 / 낙관적 동시성 | 레이스 컨디션 / 이중 지출 방지 |
| 확장 | 수직 → 수평 → 샤딩 | 단일 노드 한계에서 분산 용량으로 전환 |
| 읽기/쓰기 분리 | 기본 + 읽기 복제본 | 읽기가 쓰기를 방해하지 않게 |
| 분산 트랜잭션 | Saga 패턴 | 블로킹 없이 최종 일관성 보장 |
| 트래픽 분산 | 로드 밸런서 + HA 장애 조치 | 단일 장애점 제거, 부하 고르게 분산 |
| 악용 방지 | 속도 제한 | 악의적인 급증으로부터 보호 |
| 가시성 | 중앙 로그, 트레이싱, 메트릭 | 대규모 환경에서 빠른 근본 원인 분석 |
TL;DR
- 동시성 → 지갑을 락한다 (레이스 컨디션)
- 수직 확장 → 큰 그릴 (제한적)
- 수평 확장 → 그릴을 여러 개 (비용이 제한)
- 읽기 복제본 → “주문 상태” 전담 직원 (쓰기 한계는 여전)
- 샤딩 → 고객을 여러 그릴에 분산 (진정한 쓰기 확장)
- Saga → 락스텝 대신 보상 (탄력적인 분산 운영)
- 로드 밸런서 → 안내원 (트래픽 고르게)
- 장애 조치 → 예비 안내원 (고가용성)
- 속도 제한 → 고객당 상한 (자원 보호)
- 가시성 → 중앙 로그/메트릭/트레이스 (버그 빠르게 찾기)
이