돈을 잃지 않는 결제 시스템을 어떻게 만들까

발행: (2026년 5월 22일 AM 08:05 GMT+9)
11 분 소요
원문: Dev.to

출처: Dev.to

10 000 TPS 지불 시스템을 어떻게 구축할까

실제 엔지니어링 문제를 잡고, 먼저 일상 언어로 reasoning을 전개한 뒤, 마지막에 그 reasoning에 해당하는 정확한 용어를 붙입니다. 전문 용어는 마지막에 등장합니다.


문제

목표: 초당 10 000건의 트랜잭션을 처리하면서 단 한 건도 놓치지 않는 결제 시스템을 만든다.


1. 동시성 – “수야” 비유

비유: 두 손님이 동시에 수야 가게에 와서 마지막 꼬치를 원한다. 판매자는 손이 두 개뿐이라 동시에 서빙할 수 없다. 같은 꼬치를 두 사람에게 건네면 한 사람은 빈손이 된다.

결제 시스템에서도 두 개의 출금 요청이 정확히 같은 순간에 같은 지갑을 건드리면 같은 일이 일어난다:

  1. 두 요청이 잔액을 읽는다.
  2. 두 요청이 충분한 금액이 있다고 판단한다.
  3. 두 요청이 차감 기록을 쓴다.

결과 → 이중 지급.

용어: 동시성 문제 – 특히 레이스 컨디션.


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에서는 실패한 트랜잭션이 어디서든 발생할 수 있다:

  • 입구(로드 밸런서)
  • 주문 접수 서버(애플리케이션 서버)
  • 결제 처리 서비스
  • 기록 저장소(데이터베이스)
  • 알림 전송 시스템(메시징)

로그, 메트릭, 트레이스가 수십 대의 머신에 흩어져 있다.

해결책:

  1. 구조화된 로깅 – 요청 ID가 포함된 JSON 로그.
  2. 중앙화 로그 집계 – ELK/EFK 스택, Loki, Splunk 등.
  3. 분산 트레이싱 – OpenTelemetry, Jaeger, Zipkin.
  4. 메트릭 & 대시보드 – Prometheus + Grafana.
  5. 알림 – PagerDuty / Opsgenie 로 지연, 오류율, 자원 포화 감시.

이렇게 하면 문제가 발생했을 때 한 곳에서 원인 파악을 시작할 수 있다.


12. 전체 구성

레이어기법왜 중요한가
동시성 제어비관적 락킹 / 낙관적 동시성레이스 컨디션 / 이중 지출 방지
확장수직 → 수평 → 샤딩단일 노드 한계에서 분산 용량으로 전환
읽기/쓰기 분리기본 + 읽기 복제본읽기가 쓰기를 방해하지 않게
분산 트랜잭션Saga 패턴블로킹 없이 최종 일관성 보장
트래픽 분산로드 밸런서 + HA 장애 조치단일 장애점 제거, 부하 고르게 분산
악용 방지속도 제한악의적인 급증으로부터 보호
가시성중앙 로그, 트레이싱, 메트릭대규모 환경에서 빠른 근본 원인 분석

TL;DR

  • 동시성 → 지갑을 락한다 (레이스 컨디션)
  • 수직 확장 → 큰 그릴 (제한적)
  • 수평 확장 → 그릴을 여러 개 (비용이 제한)
  • 읽기 복제본 → “주문 상태” 전담 직원 (쓰기 한계는 여전)
  • 샤딩 → 고객을 여러 그릴에 분산 (진정한 쓰기 확장)
  • Saga → 락스텝 대신 보상 (탄력적인 분산 운영)
  • 로드 밸런서 → 안내원 (트래픽 고르게)
  • 장애 조치 → 예비 안내원 (고가용성)
  • 속도 제한 → 고객당 상한 (자원 보호)
  • 가시성 → 중앙 로그/메트릭/트레이스 (버그 빠르게 찾기)

0 조회
Back to Blog

관련 글

더 보기 »

베네수엘라의 디지털 크리에이터는 당신의 BS 솔루션을 필요로 하지 않는다.

우리가 실제로 해결하려던 문제 초기 시도 내 플랫폼의 결제 시스템은 PayPal, Stripe, Gumroad와 같은 제3자 서비스를 이용합니다. 이 서비스들은 결제 흐름을 처리하고, 결제 성공 여부를 내 시스템에 알리는 웹훅을 보냅니다. 그러나 이 웹훅이 전송되지 않거나, 결제 상태가 정확히 반영되지 않는 경우가 발생했습니다. ### 문제 정의 1. **결제 상태 동기화 실패** - 사용자가 결제를 완료했음에도 불구하고, 내 데이터베이스에 결제 완료가 기록되지 않음. - 반대로, 결제 실패가 있었음에도 “결제 완료”로 표시되는 경우가 있음. 2. **웹훅 신뢰성 부족** - PayPal, Stripe, Gumroad 모두 재시도 메커니즘을 제공하지만, 내 서버가 일시적인 오류(예: 500 오류, 타임아웃)로 인해 웹훅을 놓치는 경우가 있음. - 웹훅이 중복 전송될 때 중복 처리를 방지하는 로직이 부재함. 3. **다중 결제 제공자 관리 복잡성** - 각 제공자는 서로 다른 API 스키마와 이벤트 타입을 사용함. - 결제 확인 로직이 제공자마다 다르게 구현돼 있어 유지보수가 어려움. ### 초기 접근 방식 1. **단일 제공자에 의존** - 처음에는 Stripe만 사용하고, Stripe의 `checkout.session.completed` 이벤트만 처리하도록 설계했습니다. - 이 접근 방식은 구현이 간단했지만, PayPal과 Gumroad 사용자를 배제하게 되었습니다. 2. **동기식 확인** - 결제 완료 직후 클라이언트에서 서버로 즉시 API 호출을 보내 결제 상태를 확인하도록 했습니다. - 그러나 네트워크 지연이나 사용자가 결제 페이지를 닫는 경우, 이 호출이 누락될 수 있었습니다. 3. **단순 재시도 로직** - 웹훅 처리 중 오류가 발생하면 5분 후에 동일한 엔드포인트로 재시도하도록 설정했습니다. - 이 방식은 재시도 간격이 고정돼 있어, 일시적인 장애가 길어질 경우 여전히 데이터가 누락될 위험이 있었습니다. ### 초기 접근 방식의 문제점 - **제공자 제한**: Stripe만 지원하면 PayPal 사용자와 Gumroad 사용자를 잃게 됩니다. - **실시간성 부족**: 클라이언트‑서버 동기 호출은 사용자의 행동에 크게 의존하므로, 결제 완료를 놓칠 가능성이 높습니다. - **재시도 한계**: 고정된 재시도 간격과 횟수는 다양한 장애 상황을 충분히 커버하지 못합니다. ### 결론 우리는 결제 상태를 **신뢰성 있게** 동기화하고, **다중 제공자**를 원활히 지원하며, **웹훅 실패**에 대한 강력한 복구 메커니즘을 갖춘 시스템이 필요합니다. 이를 위해 다음과 같은 설계 원칙을 채택했습니다.

크로스 펑셔널 기술 문제 풀기 위한 역량 성장 팁

가장 먼저 기억해야 할 한 문장: Cross Functional 기술 문제는 조율만으로 풀리지 않는다고 믿습니다. 문제를 다시 정의하고, 구조화하고, 사람이 움직일 수 있는 실행 구조로 바꾸는 역량이 있어야 비로소 근본적으로 해결되는 경험을 해왔습니다. 이 문장이 중요한 이유는, 많은...