결제 시스템
- 11.1 문제 이해 및 설계 범위 확정
- 11.2 개략적 설계안 제시
- 11.3 상세 설계
- 11.4 마무리
결제 시스템은 아마존 같은 이커머스에서 "결제하기" 버튼을 누르면 뒤에서 일어나는 일들이야. 돈이 오가는 시스템이라 정확성이 생명이고, 장애가 나도 돈이 허공에 사라지면 안 되지. PSP(Payment Service Provider) 연동, 멱등성, 재시도가 핵심 키워드야.
기능 요구사항을 보면:
- 고객의 결제 요청을 처리한다 (신용카드, 직불카드 등)
- 판매자에게 **정산(payout)**을 수행한다
- 결제 내역을 조회할 수 있다
비기능 요구사항은:
- 안정성과 내결함성 — 결제 실패 시 재시도, 장애 시에도 데이터 유실 없음
- 정확히 한 번 처리 — 같은 결제가 두 번 처리되면 큰일이야 (고객 돈이 두 번 빠져나감)
- 규정 준수 — PCI DSS 등 결제 관련 보안 규정
규모 — 하루 결제 100만 건이면 초당 약 12건. QPS 자체는 높지 않지만, 한 건 한 건이 돈과 직결되니까 정확성의 무게가 다르지.
대부분의 회사는 결제를 직접 처리하지 않아. Stripe, PayPal, Toss Payments 같은 PSP에게 위임하지. PSP가 카드사, 은행과의 실제 통신을 처리해줘.
우리 시스템의 역할은 PSP에게 결제 요청을 보내고, 결과를 받아서 처리하는 거야.
결제 흐름(Pay-in)을 보면:
- 사용자가 "결제하기" 클릭
- **결제 서비스(Payment Service)**가 결제 이벤트를 생성하고 DB에 기록 (상태:
PENDING) - PSP에게 결제 요청을 전송
- PSP가 실제 결제 처리 (카드사 승인 등)
- PSP가 결과를 **웹훅(webhook)**으로 통보 — 성공 또는 실패
- 결제 서비스가 DB의 상태를
SUCCESS또는FAILED로 갱신 - 후속 처리 — 주문 상태 갱신, 이메일 발송 등
정산 흐름(Pay-out)은 판매자에게 돈을 보내는 과정이야. 주기적으로(일별, 주별) 판매 금액을 집계해서 PSP를 통해 판매자의 계좌로 송금하지. 수수료 차감도 여기서 처리해.
모든 금융 거래를 기록하는 이중 분개(double-entry bookkeeping) 원장이 필요해. 모든 거래는 **차변(debit)**과 **대변(credit)**이 쌍으로 기록돼. 차변과 대변의 합이 항상 같아야 하지. 이게 "잔고가 안 맞는다"를 감지하는 안전장치야.
**멱등성(Idempotency)**은 결제 시스템의 가장 중요한 속성이야. 같은 요청을 여러 번 보내도 결과가 한 번 보낸 것과 같아야 하지.
왜 필요한가? 네트워크는 불안정하거든. 클라이언트가 결제 요청을 보냈는데 응답을 못 받으면 재시도할 수 있어. 이때 서버가 "이미 처리된 요청"을 인식하지 못하면 이중 결제가 발생하지.
구현 방법 — 각 결제 요청에 **멱등성 키(idempotency key)**를 부여해 (UUID 등). 서버는 이 키로 이미 처리된 요청인지 확인하지.
POST /payments
Idempotency-Key: 3f8c2d1a-...
서버의 처리 로직:
- 멱등성 키로 DB를 조회
- 이미 존재하면 → 기존 결과를 그대로 반환
- 존재하지 않으면 → 새 결제 처리 + 멱등성 키를 DB에 저장
여기서 중요한 건 **멱등성 키 확인과 결제 처리가 원자적(atomic)**이어야 한다는 거야. DB 트랜잭션이나 유니크 제약 조건으로 보장하지.
PSP 호출이 실패했을 때 무작정 재시도하면 안 돼.
지수 백오프(Exponential Backoff) — 1초, 2초, 4초, 8초... 재시도 간격을 점점 늘려. PSP가 과부하일 때 즉시 재시도하면 상황을 악화시키니까.
최대 재시도 횟수 — 무한히 재시도하면 안 돼. 일정 횟수 이상 실패하면 수동 처리 큐로 넘기지.
재시도 가능한 에러만 재시도 — "카드 번호 틀림" 같은 건 재시도해도 소용없어. 네트워크 타임아웃, 서버 에러(5xx) 같은 일시적 장애만 재시도하지.
민감한 카드 정보를 우리 서버가 직접 다루면 PCI DSS 규정을 지키기 위한 보안 비용이 엄청나. 그래서 대부분 **PSP의 호스팅 결제 페이지(hosted payment page)**를 사용해.
흐름은 이래:
- 클라이언트가 결제 서비스에 결제 의향을 알림
- 결제 서비스가 PSP에 결제 토큰을 요청
- PSP가 토큰과 결제 페이지 URL을 반환
- 클라이언트가 PSP의 결제 페이지에서 직접 카드 정보를 입력
- PSP가 결제를 처리하고 결과를 웹훅으로 통보
이러면 카드 번호가 우리 서버를 거치지 않으니 PCI 범위가 줄어들지.
결제의 상태는 여러 단계를 거쳐.
NOT_STARTED → EXECUTING → SUCCESS / FAILED
각 상태 전이를 DB에 기록하고, 상태별로 할 수 있는 동작을 제한해. SUCCESS 상태의 결제를 다시 실행하려는 시도는 차단하지.
멱등성은 "중복 실행 방지"이고, "누락 방지"는 별도로 처리해야 해.
결제 서비스가 죽어서 PSP의 웹훅을 못 받으면? 웹훅 재시도 — PSP는 웹훅 전달에 실패하면 일정 횟수 재시도해. 우리 쪽에서는 같은 웹훅을 여러 번 받아도 멱등하게 처리하지.
웹훅 재시도도 실패하면? 주기적 폴링 — 결제 서비스가 주기적으로 PSP의 API를 호출해서 미결 상태의 결제 결과를 확인하지.
이 조합(멱등성 키 + 웹훅 재시도 + 주기적 폴링)으로 적어도 한 번 전달 + 멱등적 처리 = 정확히 한 번 처리를 달성해.
결제 DB, 원장, PSP의 기록이 항상 일치해야 해. 이를 검증하기 위해 정산 대사(reconciliation) 프로세스를 주기적으로 돌리지. PSP에서 제공하는 결제 내역 파일(settlement file)과 내부 DB의 기록을 비교해서 차이가 있으면 조사해.
결제 시스템은 "돈"이라는 특수한 도메인 때문에 일반적인 시스템 설계보다 정확성과 안정성에 대한 요구가 극단적이야. 멱등성, 재시도, 원장, 대사 같은 개념은 결제뿐 아니라 12장(전자 지갑)에서도 그대로 이어지지.
정리
11장 읽고 기억할 거 세 가지:
- 멱등성 키로 중복 결제를 방지해. 네트워크 재시도로 같은 요청이 여러 번 와도 한 번만 처리되도록. 멱등성 키 확인과 결제 처리는 원자적이어야 하지.
- PSP의 호스팅 결제 페이지를 쓰면 카드 정보를 직접 다루지 않아 PCI 범위가 줄어들어. 대부분의 서비스가 이 방식을 채택하지.
- 정확히 한 번 처리는 "적어도 한 번 전달(재시도) + 멱등적 처리"의 조합이야. 웹훅 재시도와 주기적 폴링으로 전달을 보장하고, 멱등성으로 중복을 걸러내지.