주문/결제 시스템에서 CP와 AP 선택하기 (feat. 블랙프라이데이)
이전 1편: 피터 도이치의 8가지 오류에서 분산 환경의 모든 고통이 물리적 한계에서 시작된다는 사실을 확인했습니다. 2편: CAP 이론에서는 "P는 선택이 아니라 전제, C와 A 중 하나를 포기해야 한다"는 트레이드오프의 수학적 정의를 짚었습니다.
이번 3편에서는 이 트레이드오프를 실제 이커머스 주문 시스템에 대입합니다. "완벽한 정합성(CP)만 고집하면 왜 서비스가 터지는가"부터, "어떻게 AP로 가되 사가 패턴과 멱등성으로 방어하는가"까지, 결제 도메인의 생존 전략을 짚습니다.
Hook: 결제창이 3초 멈추면 매출이 증발한다 — 그래서 우리는 정합성을 한 박자 늦추기로 했다.
1. 실무의 고민 — 왜 CP만 고집하면 서비스가 터질까?
2편에서 정리한 대로, 분산 시스템은 P(네트워크 장애)를 무조건 감수해야 하고, C(일관성)와 A(가용성) 중 하나를 선택해야 합니다. 정합성을 우선하는 CP 모델은 모든 트랜잭션이 무결하다는 강력한 보장을 줍니다.
이론적 모델과 실무 구현의 차이. 이 보장을 구현하는 가장 정직한 이론 모델은 2PC(Two-Phase Commit) 입니다. 하지만 2PC는 락 블로킹·Coordinator SPOF·확장성 한계로 실무에서 거의 쓰이지 않습니다. 대신 다음 4가지가 실제 CP 시스템의 구현 방식입니다.
- Raft / Paxos 합의 — etcd, ZooKeeper, Consul, CockroachDB, TiDB. 과반수 노드 동의로 쓰기 확정.
- 단일 리더 + 동기 복제 — Google Spanner, YugabyteDB. 한 노드가 직렬화 + 복제본 동기 커밋.
- Quorum 읽기/쓰기 — DynamoDB (강한 일관성 모드), Cassandra (QUORUM). W+R > N 공식.
- CP 기반 분산 락 — ZooKeeper/etcd의 합의 알고리즘 기반 락. 단순 Redis SETNX는 AP 성격이라 split-brain 가능성이 있어 CP 도메인에 부적합합니다.
이 절에서는 2PC를 통해 CP가 왜 어려운지를 먼저 짪고, 실무 대안(특히 합의 알고리즘)은 4편에서 깊이 다룹니다.
2PC의 동작 원리
2PC는 이름 그대로 두 단계로 트랜잭션을 확정합니다.
sequenceDiagram
participant App as 주문 API
participant Coord as Coordinator
participant DB1 as 재고 DB
participant DB2 as 결제 DB
App->>Coord: BEGIN
Note over Coord,DB2: Phase 1: PREPARE
Coord->>DB1: PREPARE (락 획득)
Coord->>DB2: PREPARE (락 획득)
DB1-->>Coord: Yes
DB2-->>Coord: Yes
Note over Coord,DB2: Phase 2: COMMIT
Coord->>DB1: COMMIT
Coord->>DB2: COMMIT
DB1-->>Coord: OK
DB2-->>Coord: OK
Note over App,DB2: 모든 락이 트랜잭션 끝까지 유지됨
핵심은 모든 참여 노드(재고 DB, 결제 DB)가 PREPARE 단계에서 락을 잡은 채 Coordinator의 COMMIT 명령을 기다린다는 점입니다. 두 DB 모두 "예"라고 답하기 전에는 어느 쪽도 락을 풀지 않습니다.
2PC의 3대 고질적 문제
이 "모두가 응답할 때까지 기다리는" 메커니즘이 대규모 트래픽에서 3가지 문제를 만듭니다.
- 블로킹(Blocking): 모든 노드가 락을 잡고 응답을 기다리므로, 단일 노드의 지연이 전체 트랜잭션 지연으로 직결됩니다.
- 단일 장애점(SPOF): Coordinator가 응답을 잃으면 모든 참여 노드는 락을 영원히 풀지 못합니다.
- 타임아웃 연쇄: 한 노드의 응답이 늦어지면 다른 트랜잭션들도 락 대기 행렬에 쌓이면서 커넥션 풀이 고갈됩니다.
블랙프라이데이 시나리오
평소 TPS 1,000, 평균 응답 200ms인 결제 시스템이 있다고 합시다. 블랙프라이데이로 트래픽이 10배(TPS 10,000) 증가하면 어떻게 될까요?
- 락 경합이 10배로 늘어남
- 평균 응답 시간 200ms → 2,000ms (10배 증가)
- TPS는 응답 시간에 반비례해 1/10로 떨어짐 → TPS 1,000 (사실상 평소 수준도 못 처리)
- 사용자는 결제창에서 3초 이상 멈춘 페이지를 보고 이탈
- 결과: 정합성은 100% 보존, 매출은 0원 + CS 폭주
이게 "정합성 100% vs 매출 0원" 딜레마의 정체입니다. CP는 데이터는 지키지만 서비스를 죽입니다.
그렇다면 어떻게 해야 할까요? 2편에서 정리한 "P는 무조건 전제"라는 사실을 받아들이고, C와 A 사이에서 A를 택하는 AP 모델로 가는 길이 열립니다. 그 첫 번째 디딤돌이 §2의 메시지 큐 + 최종 일관성 입니다.
2. 주문 시스템은 AP가 정답일까? — 메시지 큐 + 최종 일관성
§1에서 봤듯, CP는 정합성을 지키지만 서비스를 죽입니다. 그럼 어떻게 해야 할까요? 답은 2편 CAP 이론으로 돌아가는 것입니다 — P는 무조건 전제이니, C와 A 중 하나를 포기해야 한다. 그리고 결제 도메인은 대부분 A(가용성) 를 선택합니다.
왜 주문 시스템은 AP인가?
"주문 자체는 거의 모든 사용자에게 성공시키자. 재고 차감·결제 정합성은 백그라운드에서 천천히 맞추자."
이 결정의 손실 함수를 보면 명확합니다.
- CP 선택 시 손실: 주문 1건 실패 = 그 사용자 영구 이탈 + CS 비용 + 결제창 멈춤으로 연쇄 이탈
- AP 선택 시 손실: 일부 주문이 뒤늦게 품절 확인 = 보상 쿠폰 + 환불. 정합성은 수 분~수 시간 내 복구
손실의 양과 가역성 모두 AP가 압도적으로 유리합니다. 그래서 "주문 접수" 단계는 무조건 가용성을 우선합니다.
메시지 큐 도입 — 동기 → 비동기 패러다임 전환
이 가용성을 구현하는 핵심 장치가 메시지 큐(Kafka, RabbitMQ 등) 입니다.
sequenceDiagram
participant User as 사용자
participant API as 주문 API
participant DB as 주문 DB
participant Queue as Kafka
participant Worker as 결제 워커
participant PayDB as 결제/재고 DB
User->>API: POST /orders
API->>DB: 주문 INSERT (단일 트랜잭션)
DB-->>API: OK
API->>Queue: order.created 이벤트 발행
API-->>User: 200 OK (50ms 이내)
Note over Queue,Worker: 비동기 처리
Worker->>Queue: consume
Worker->>PayDB: 재고 차감 + 결제 시도
PayDB-->>Worker: OK / 품절
Worker-->>Queue: 처리 결과 발행
핵심 차이는 DB 커밋과 외부 시스템 호출이 분리되었다는 점입니다.
- 이전 (CP/동기): API → DB 커밋 → 응답 (트랜잭션 끝나야 응답, 락 유지)
- 변경 (AP/비동기): API → DB 단일 커밋 → 큐 Publish → 200 OK (50ms 내) → 워커가 비동기로 처리
최종 일관성(Eventual Consistency)
이 모델에서 데이터는 "충분한 시간이 지나면 같은 상태에 도달한다" 는 보장을 합니다. 이를 최종 일관성(Eventual Consistency) 이라 부릅니다. 강한 일관성처럼 매 시점 일치를 보장하지는 않지만, 워커가 큐를 다 비울 때까지(보통 수 초~수 분) 시스템은 일관된 상태로 수렴합니다.
1편의 회복탄력성 4패턴 — 최종 일관성의 4대 장치
최종 일관성은 "마법"이 아닙니다. 1편 §5에서 다룬 회복탄력성 4패턴이 최종 일관성을 가능하게 하는 장치로 다시 등장합니다.
| 1편 회복탄력성 패턴 | 3편에서 재매핑 | 결제 도메인 적용 |
|---|---|---|
| Timeout & Retry with Exponential Backoff | 워커 재시도 정책 | 결제 API 타임아웃 후 지수 백오프로 재시도 |
| Circuit Breaker | 결제사 장애 격리 | 토스/네이버페이 장애 시 빠르게 실패, 큐에 남겨둠 |
| Idempotency Key | 중복 결제/차감 방지 | 같은 주문 ID로 워커가 N번 실행돼도 1번만 차감 |
| 비동기 메시징 | 주문-결제 분리 | 메시지 큐 자체가 이 패턴의 인프라 |
이 표가 1편과 3편을 잇는 다리입니다. 1편에서 "이런 패턴이 있다"로 끝났다면, 3편에서는 "이 패턴들이 모여 최종 일관성을 만든다"로 완성됩니다.
여기서 한 가지 결정이 남습니다. "주문 접수 시점에 재고를 어떻게 다룰 것인가" — 동기 선차감으로 사전 차단할 것인가, 비동기 후처리(사가 + 멱등성)로 흡수할 것인가. §3에서 이 결정의 1차 방어선을, §4에서 2차 방어선을 다룹니다.
3. 1차 방어선 — Redis 동기 선차감
§2의 메시지 큐 모델은 결제·알림 등 후처리를 비동기로 잘 흡수하지만, "주문 접수 시점"의 재고 처리는 별개의 결정입니다. 이 결정에서 우리는 두 선택지 앞에 섭니다.
- 비동기 후처리 모델: §2의 흐름대로 주문은 일단 받고, 워커가 재고 차감 시도. §2 시나리오의 "50,000명 → 49,000건 후알림"이 여기서 발생.
- 동기 선차감 모델: 주문 API가 Redis의 원자적 카운터를 즉시 차감. 0 이하면 주문 자체를 거부. 사전 차단.
실제 이커머스 시스템은 거의 다 후자가 1차 방어선입니다. 핵심은 Redis의 DECR 명령이 atomic이라는 점입니다.
sequenceDiagram
participant User as 사용자
participant API as 주문 API
participant Redis as Redis (재고 카운터)
participant DB as 주문 DB
User->>API: POST /orders
API->>Redis: DECR stock:product-123
alt 재고 있음 (결과값 >= 0)
Redis-->>API: 999
API->>DB: 주문 INSERT
DB-->>API: OK
API-->>User: 200 OK
else 재고 없음 (결과값 < 0)
Redis-->>API: -1
API->>Redis: INCR (보상)
API-->>User: 409 Sold Out
end
왜 Redis DECR인가?
- Atomic: 50,000명이 동시에 눌러도 단일 명령이라 race condition 없음. 락 불필요.
- 빠름: 1~2ms 내 응답. 50ms 응답 시간 안에 충분히 들어감.
- 보상이 한 줄: DB INSERT가 실패하면
INCR한 번으로 재고 복구. 사가 패턴이 한 줄짜리로 축소.
이 모델에서 "50,000명 → 49,000건 후알림" 시나리오는 99% 사전 차단됩니다. 49,000명은 애초에 "품절" 응답을 받고 끝나기 때문에 후알림 UX가 발생할 일이 없습니다.
선차감이 막지 못하는 케이스 (그래서 §4가 필요함)
선차감이 완벽하지는 않습니다. 다음 케이스에서는 여전히 후처리가 필요합니다.
- Redis 자체 장애: Redis가 죽으면 선차감 불가 → DB 락 fallback 또는 비동기 큐 후처리
- Redis ↔ DB 동기화 실패: Redis 카운터는 100인데 DB 실제 재고는 80 (반대 방향도 가능) → 어긋난 차감 발생
- 멀티 DC / 교차 리전: 리전별 카운터 분리 시 합산 누락
- 한정판 race: 카운터는 atomic이지만 그 이전 단계(예: 결제 진입 대기열)에서 race 가능
이런 케이스들이 2차 방어선(사가 + 멱등성 + UX 4박자) 이 필요한 이유입니다. §4에서 다룹니다.
단, Redis 자체의 한계. 위 DECR/INCR 모델은 Redis가 AP 성격이라 master-replica failover 시 split-brain 가능성이 있습니다. 한정판·럭키드로우처럼 오버셀이 절대 안 되는 도메인이라면, §5에서 다룰 CP 기반 분산 락(ZooKeeper/etcd) 으로 가야 합니다.
4. 2차 방어선 — 그래도 생기는 품절 (사가 + 멱등성 + UX 4박자)
§3의 동기 선차감으로 1차 방어선을 깔았지만, 선차감이 막지 못하는 케이스에서는 여전히 부작용이 발생합니다. 이 절에서는 그 부작용을 흡수하는 2차 방어선을 다룹니다.
시나리오: 선차감 실패 케이스 (한정판 1,000개, 동시 50,000명 주문)
10:00:00.000 오픈 → 500ms 안에 50,000명이 동시 주문 버튼을 누릅니다.
- Redis 카운터가 잠시 응답 불능 상태였고, fallback 경로로 일부 주문이 통과됨
- 또는 Redis 카운터와 DB 실재고가 어긋나 1,200건이 통과됨
- 워커가 1,000건만 결제 성공, 200~49,000건은 "재고 없음"
- 일부 사용자에게 "결제됐는데 품절" 상태 발생
"응답은 성공, 결과는 실패"인 주문이 쌓입니다. 이걸 방어하지 않으면 서비스는 신뢰를 잃습니다.
사가 패턴(Saga) — 보상 트랜잭션으로 부작용 제어
사가 패턴은 여러 단계의 트랜잭션을 순차 실행하되, 중간 단계 실패 시 앞서 성공한 단계를 되돌리는 보상 트랜잭션(Compensating Transaction) 을 실행합니다.
sequenceDiagram
participant Orch as Saga Orchestrator
participant Stock as 재고 서비스
participant Pay as 결제 서비스
participant Noti as 알림 서비스
Note over Orch,Noti: 정상 흐름
Orch->>Stock: T1. 재고 차감
Stock-->>Orch: OK
Orch->>Pay: T2. 결제 실행
Pay-->>Orch: OK
Orch->>Noti: T3. 확정 알림
Noti-->>Orch: OK
Note over Orch,Noti: 보상 흐름 (T2 실패 시)
Orch->>Pay: C1. 결제 취소
Pay-->>Orch: OK
Orch->>Stock: C2. 재고 복구
Stock-->>Orch: OK
Orch->>Noti: C3. 품절+환불 알림
Noti-->>Orch: OK
두 가지 구현 방식이 있습니다.
- 코레오그래피(Choreography): 각 서비스가 이벤트로 자율 보상. 서비스 3개 이하에서 단순.
- 오케스트레이션(Orchestration): 중앙 Orchestrator가 단계 제어. 4개+ 또는 복잡한 분기에서 권장.
멱등성(Idempotency) — "같은 요청 N번 = 1번"
사가가 보상 흐름을 안정적으로 돌리려면 워커가 같은 작업을 여러 번 실행해도 결과가 동일해야 합니다. 이를 멱등성이라 부르며, 1편 §5에서 잠깐 짚었던 "AP의 4번째 패턴"을 결제 도메인의 생존 전략으로 격상시킨 것입니다.
sequenceDiagram
participant Worker as 결제 워커
participant Cache as 멱등 캐시 (Redis)
participant DB as 주문 DB
Worker->>Cache: SET order-12345 "processing" NX EX 600
alt 캐시 HIT
Cache-->>Worker: nil
Worker->>Worker: 스킵 (중복 방지)
else 캐시 미스
Cache-->>Worker: OK
Worker->>DB: 재고 차감 + 결제
DB-->>Worker: OK
Worker->>Cache: SET order-12345 "done" EX 86400
end
핵심은 주문 ID를 Redis SETNX 또는 DB 유니크 제약으로 1차 방어선에 세우는 것입니다. 재시도 폭주, 워커 중복 실행, 네트워크 재전송 — 이 모든 상황에서 같은 주문은 정확히 1번만 결제되고 차감됩니다.
UX 4박자 — 시스템이 못 막은 것을 언어로 막기
사가와 멱등성으로 "결제 정합성"은 지킬 수 있지만, 사용자가 경험하는 "주문 후 품절"의 충격은 시스템이 흡수하지 못합니다. 이건 언어로 방어합니다.
- 즉시 알림: 알림톡/푸시로 "선착순 초과로 주문이 취소되었으며, 즉시 환불 처리되었습니다" 투명 안내
- 즉시 환불 고지: "영업일 3~5일 환불" 문구 최우선 배치
- 보상 제시: 대체 상품 추천 or 사과 쿠폰
- 용어 방어: UI에서 처음부터 "주문 확정" 대신 "주문 접수" 사용
1편 §5의 회복탄력성 패턴이 시스템 레벨 방어였다면, UX 4박자는 사람 레벨 방어입니다.
§4 정리: §3의 동기 선차감으로 99%의 부작용을 사전 차단하고, 선이 통과한 1% 를 사가(보상) + 멱등성(중복 방지) + UX 4박자(사람 방어)로 제어합니다.
이제 남은 질문은 하나입니다. "내 서비스의 모든 도메인이 AP여야 하는가, 어떤 건 CP여야 하는가?" §5에서 도메인별 결정 매트릭스로 답합니다.
5. 마치며 — 비즈니스 요구사항별 트레이드오프
3편을 통해 우리는 주문 시스템이 AP + 1차 선차감(§3) + 2차 사가/멱등성/UX(§4) 의 이중 방어선으로 구성됨을 확인했습니다. 그렇다면 한 시스템 안에서도 모든 도메인이 같은 선택을 해야 할까요?
답은 "아니다"입니다. 같은 서비스 안에서도 도메인별로 우선순위가 다릅니다.
| 도메인 / 기능 | 우선순위 | 선택 | 적용 패턴 | 이유 |
|---|---|---|---|---|
| 결제 정산 (돈 이동) | C > A | CP | 2PC | 돈 틀어지면 복구 불가 |
| 재고 차감 (대략 정확) | A > C | AP + §3 Redis DECR | atomic counter, 약간의 오버셀 허용 | 일반 상품, 빠른 응답 우선 |
| 재고 차감 (정확한 카운트) | C > A | CP + ZooKeeper/etcd 락 | 분산 락 + 단일 DB | 한정판·럭키드로우, 오버셀 제로 |
| 주문 접수 (버스트) | A > C | AP | §2 큐 + §3 선차감 + §4 사가 | 응답 실패 = 매출 손실 |
| 상품 조회 / 상세 | A > C | AP | 캐시 + 최종일관성 | 약간 틀어져도 무관 |
| 알림 발송 | A > C | AP | §4 멱등성 + UX 4박자 | 중복·유실은 멱등으로 방어 |
| 쿠폰 사용 (선착순) | A ≈ C | 하이브리드 | §3 선차감 + 비동기 결과 | 정확성과 속도 동시 요구 |
결정 질문 3개 — 내 도메인에 대입해보기
"그래서 내 도메인은 어디에 해당하나?"에 답할 수 있는 결정 질문 3가지입니다.
"내 서비스가 다운되면 손실이 더 큰가, 데이터가 1초 틀어지면 손실이 더 큰가?"
- 다운 손실이 더 큼 → AP 우선
- 데이터 틀어짐 손실이 더 큼 → CP 우선
"틀어진 데이터를 나중에 복구할 수 있는가? (되돌리기 가능성)"
- 되돌릴 수 있음 (재고, 주문) → AP 가능
- 되돌릴 수 없음 (돈, 감사 로그) → CP 필수
"사용자가 화내는 건 '느림'인가 '틀림'인가?"
- 보통 느림에 더 화냄 → AP 우선 (응답은 살리고 사후 보정)
- 정확성·신뢰가 핵심 (금융, 의료) → CP 우선 (느려도 정확히)
3편의 핵심 메시지는 이겁니다. "분산 시스템은 늘 무언가를 포기해야 한다"는 2편 CAP의 결론을 받아들이되, 포기할 것을 도메인별로 정확히 선택하는 게 엔지니어의 일이다.
Outro
3편을 마치며 한 가지 짚고 가야 합니다. §3의 동기 선차감, §4의 사가 + 멱등성, §5의 도메인별 매트릭스 — 이 모든 결정은 "네트워크에 장애가 발생했을 때(P)"를 가정한 것입니다. 1편에서 다룬 8가지 오류 중 #1(신뢰), #2(지연), #3(대역폭)이 작동하는 시나리오죠.
하지만 분산 시스템은 "평상시(Else)"에도 트레이드오프에 직면한다는 사실을 짚은 이론이 있습니다. PACELC 이론입니다. 네트워크가 멀쩡한 일상에서도 지연(Latency) 과 일관성(Consistency) 사이에서 선택을 강요당합니다.
4편에서는 PACELC를 깊이 다루고, "정말로 강한 일관성이 필요한 도메인"을 위해 Quorum(정족수) 과 Raft 합의 알고리즘이 어떻게 동작하는지 살펴봅니다.
다음 편 예고 — 4편: PACELC와 Quorum/Raft 합의 알고리즘
이번 3편에서 우리는 "CP만 고집하면 서비스가 죽는다" 는 현실, 그리고 이를 1차 방어선(§3 Redis 동기 선차감) + 2차 방어선(§4 사가 + 멱등성 + UX 4박자) 의 이중 구조로 풀어내는 실무 전략을 정리했습니다. 4편에서는 한 발 더 깊이 들어갑니다. 평상시(Else)에도 트레이드오프가 존재한다는 PACELC 이론, 그리고 "정말로 강한 일관성이 필요한 도메인"을 위해 사용할 수 있는 Quorum(정족수) 과 Raft 합의 알고리즘의 동작 원리를 다룹니다.
'CS > 분산시스템' 카테고리의 다른 글
| CAP을 넘어 PACELC로: 평상시 트레이드오프와 합의 알고리즘 (0) | 2026.10.09 |
|---|---|
| 빛과 전파의 한계에서 시작하는 분산 시스템: 피터 도이치의 8가지 오류와 현실 (0) | 2026.10.09 |
| CAP 이론, "P를 보장한다"는 것의 진짜 의미 (0) | 2026.10.08 |
댓글