쿠팡급 이커머스 시스템 설계: 상품·재고·주문·결제·배송 추적
마지막 한 개처럼 보이는 상품을 두 사람이 거의 동시에 결제하면 누구의 주문을 확정할 것인가. 쿠팡급 이커머스 시스템 설계에서 검색 화면의 재고는 빠른 안내값이고, 결제 시점의 재고는 돈과 수량을 바꾸는 판정값이다. 둘을 같은 데이터와 같은 최신성 기준에 맡기지 않는 것이 이 글의 출발점이다.
모바일 앱과 웹의 구매자가 상품을 찾고, 결제하고, 배송과 반품을 확인할 때 시스템이 지켜야 할 세 약속을 따라간다. 독자는 각 행동에서 요청이 어디로 이동하고, 어떤 기록이 판정을 내리며, 실패가 어떤 상태로 수습되는지 요청 접점과 데이터 모델로 확인할 수 있다.
20초 핵심 요약
- 무엇: 검색용 상품 사본과 돈·수량을 바꾸는 거래 기록을 분리하고, 검색·결제·배송/반품을 하나의 상태 흐름으로 잇는다.
- 왜: 오래된 검색 재고, 중복 주문과 결제, 뒤집힌 배송 상태는 품절 취소·이중 청구 오인·환불 지연으로 이어진다.
- 어떻게: 결제 때 재고를 다시 확인하고 짧게 예약하며, 주문 조정자가 결제와 출고를 진행하고 실패한 단계는 해제·취소 같은 반대 동작으로 수습한다.
세 가지 사용자 약속과 설계 가정을 먼저 고정한다
구매자는 검색어와 필터로 상품을 찾고 옵션·가격·도착 예정일을 비교한다. 검색 결과는 빨라야 하지만 재고 안내가 아주 잠깐 늦을 수 있다. 반대로 결제 버튼을 누른 뒤에는 가격, 수량과 결제 금액이 정확해야 하며, 네트워크가 끊겨 다시 눌러도 주문은 하나여야 한다.
배송 화면에는 사건이 발생한 순서가 보여야 한다. 택배사 통지가 늦게 도착해도 배송 완료가 다시 이동 중으로 돌아가서는 안 된다. 반품 역시 접수, 회수, 검수, 환불을 나눠 사용자가 멈춘 지점을 알 수 있어야 한다.
여기에는 구매자 외에도 상품·가격을 바꾸는 판매자와 운영자, 입고·포장·출고 사건을 만드는 창고 작업자, 배송 사건을 보내는 택배사, 주문을 확인하는 고객 지원 담당자가 참여한다. 지원 담당자는 주문 이력은 볼 수 있어도 카드 원문처럼 업무에 필요 없는 민감정보는 보지 못해야 한다.
범위는 상품 카탈로그, 검색 색인, 재고 예약, 주문과 결제, 배송·반품, 핫딜 보호와 복구다. 추천 모델, 광고 경매, 판매자 정산, 창고 최적 경로와 라스트마일 배차는 연결점만 남기고 제외한다.
이 설계의 정확성 우선순위는 결제 금액과 주문 이력 > 재고 확정 > 배송 현재 상태 > 검색 결과 최신성이다. 검색을 계속 제공하기 위해 잠시 오래된 사본은 허용하지만, 돈과 수량의 판정까지 그 사본에 맡기지는 않는다.
API(Application Programming Interface, 사용자의 앱·웹과 서버가 정해진 형식으로 요청과 결과를 주고받는 접점)는 이 글에서 검색·결제·배송 행동을 서버 처리로 연결한다. 같은 행동을 재시도했을 때의 결과와 실패 응답까지 합의해야 사용자가 중복 주문이나 멈춘 화면을 피할 수 있으므로 필요하다.
이 글은 2026년 8월 15일 기준 OpenSearch, AWS, Stripe, Apache Kafka, GS1, OWASP와 PCI Security Standards Council의 공식 자료를 대조한 논리 설계다. 특정 기업의 비공개 구조나 실제 성능을 재현하지 않았으며, 아래 트래픽·지연·예약 시간은 선택을 시험하기 위한 설계 가정이다.
한 상품의 순간 경쟁이 규모 선택을 바꾼다
아래 값은 쿠팡의 운영 수치가 아니라 설계 연습용 입력이다. RPS(Requests Per Second)는 1초에 들어오는 요청 수이며, 평균과 순간 피크가 어느 저장 경계를 압박하는지 보기 위해 쓴다.
| 입력 가정 | 값 |
|---|---|
| 일간 활성 사용자 | 5천만 명 |
| 하루 검색·목록·상세 읽기 | 10억 회 |
| 하루 주문 | 500만 건 |
| 주문당 평균 품목 | 2.5개 |
| 상품 / 재고 관리 단위 | 1억 개 / 5억 개 |
| 활성 창고 재고 행 | 20억 개 |
| 읽기 / 주문 피크 | 평균의 10배 / 20배 |
| 핫딜 단일 상품 집중 | 평시 해당 상품의 1,000배 |
읽기는 평균 10억 / 86,400 ≈ 11,574 RPS, 피크 약 116,000 RPS다. 주문은 평균 약 58건/초, 20배 피크 약 1,160건/초다. 주문당 2.5개 품목을 예약하면 피크 예약 쓰기는 약 2,900건/초가 된다.
주문과 품목·상태 이력을 합쳐 주문당 4KB로 잡으면 하루 약 20GB, 연간 약 7.3TB다. 주문당 결제·재고·배송 사건 15개가 각각 1KB라면 하루 약 75GB, 연간 약 27.4TB다. 모두 복제본, 인덱스, 백업과 압축을 제외한 논리 크기다.
검색 문서가 상품당 평균 2KB라면 원문은 약 200GB다. 검색을 위한 역색인, 복제와 여유 공간을 원문의 4~8배로 가정하면 0.8~1.6TB가 필요하다. 실제 배수는 검색 필드와 분석 방법으로 측정해야 한다.
이 평균값만 보고 데이터베이스를 나누면 핫딜을 놓친다. 주문 피크가 10배 늘어도 서로 다른 상품에 고르게 퍼지면 저장 구역을 늘릴 수 있다. 그러나 한 상품에 몰리면 (상품, 창고) 재고 한 행의 순차 판정이 먼저 막힌다. 평균 주문 품목이 2.5개에서 10개가 되면 예약 쓰기와 주문 단계도 네 배가 되므로 주문 수뿐 아니라 품목·창고별 대기 시간을 봐야 한다.
찾을 때는 빠르게: 검색 사본의 지연을 거래에서 격리한다
이 행동을 서버 처리로 잇는 검색 API는 다음과 같다.
GET /v1/search?q=무선이어폰&filters=brand:Hunt&cursor=...
GET /v1/products/{product_id}
GET /v1/products/{product_id}/availability?postal_code=06236
SKU(Stock Keeping Unit)는 색상과 크기까지 구분한 재고 관리 단위이며, 어떤 옵션의 수량을 확인할지 정하는 키라서 필요하다. 검색 응답에는 상품 ID, 표시용 최저가, 이미지, 판매 가능 힌트, 색인 시각과 다음 페이지 위치를 둔다.
- 사용자가 검색어를 보내면 API 입구가 인증 여부, 요청 크기와 사용자·기기별 요청 속도를 검사한다.
- 검색 서비스는 상품 원본을 조회에 맞게 펼쳐 둔 검색용 사본에서 키워드·필터·정렬 결과를 찾는다.
- 표시용 가격과 판매 상태도 검색 문서에 함께 둔다.
fan-out은 요청 하나가 많은 하위 요청으로 퍼지는 현상이며, 후보마다 상품·가격·재고 원본을 부르면 한 번의 검색이 지연과 장애를 크게 증폭하므로 피한다. - 사용자가 상세를 열면 상품 원본과 캐시에서 설명·옵션을 읽고 선택한 SKU와 우편번호의 비교적 최신 재고를 조회한다.
- 응답에는 재고 힌트를 계산한 시각을 표시하고 결제에서 다시 확인한다.
상품 변경은 원본 저장 → 변경 사건 발행 → 검색 문서 재생성 → 색인 갱신 순서로 이어진다. OpenSearch 공식 문서에 따르면 문서는 색인 뒤 갱신 과정을 거쳐 검색 가능해지며, 잦은 강제 갱신은 성능을 해칠 수 있다. 따라서 검색 결과가 잠깐 늦는 비용은 받아들이고 결제 판단을 색인에 맡기지 않는다.
결제할 때는 한 번만 정확하게: 예약과 결제를 한 주문으로 수렴시킨다
멱등성은 같은 업무 요청이 여러 번 도착해도 한 번 처리한 것과 같은 결과를 만드는 성질이다. 새로고침과 네트워크 재시도가 중복 주문·중복 결제를 만들지 않게 하므로 주문 생성부터 적용한다.
POST /v1/checkouts
Idempotency-Key: checkout:{user_id}:{client_attempt_id}
{
"cart_version": 17,
"items": [{"sku_id":"sku_42","quantity":2}],
"shipping_address_id":"addr_7",
"payment_method_token":"pm_provider_token"
}
{
"order_id": "ord_01...",
"status": "PROCESSING",
"price_snapshot": {"currency":"KRW","total":129000},
"next_action": {"type":"POLL","url":"/v1/orders/ord_01..."}
}
서버는 멱등 키와 요청 본문의 해시를 함께 저장한다. 같은 키와 같은 본문이면 기존 주문을 돌려주고, 같은 키인데 본문이 다르면 409 Conflict로 거부한다. 저장 기간이 끝난 뒤 같은 키가 새 요청이 될 수 있으므로 주문 ID와 결제사 ID에도 영구 고유 제약을 둔다.
다음 그림은 왼쪽의 결제 버튼에서 시작한다. 가격 재검증 → 재고 예약 → 결제 요청의 번호를 따라간 뒤, 승인 결과가 확정과 해제 중 어느 분기로 이어지는지 읽으면 된다.

이 경계에서는 검색 사본의 재고가 아니라 권위 재고 기록이 확정할 주문을 판정한다. 사용자에게는 주문 확정, 추가 인증, 처리 중 또는 품절 가운데 하나를 돌려주고, 결과가 불명확하면 새 결제를 만들지 않는다.
결제 흐름은 다음 순서다.
- 주문 조정자는 멱등 키로 기존 시도를 찾는다. 없으면
PENDING주문과 당시 상품명·가격·세금·배송비를 한 거래로 저장한다. - 현재 가격과 장바구니 버전을 다시 확인한다. 금액이 바뀌면 결제를 멈추고 새 금액에 대한 사용자 확인을 받는다.
- 재고 저장소는
(sku_id, warehouse_id)의 가용량이 요청량 이상일 때만 수량을 줄이고 짧은RESERVED예약을 만든다. 장바구니에 담은 시점에는 재고를 잡지 않는다. - 결제 어댑터는
order_id + payment_attempt_no를 외부 결제 멱등 키로 보내 승인 또는 추가 인증을 요청한다. 애플리케이션에는 카드 원문 대신 결제사가 발급한 토큰만 둔다. - 승인이 성공하면 예약을
CONFIRMED, 주문을CONFIRMED로 바꾼다. 결과가 불명확하면 새 결제를 만들지 않고 같은 시도를 조회하거나 같은 키로 재시도하며PAYMENT_PENDING을 보여준다. - 결제가 실패하면 예약을
RELEASED, 주문을PAYMENT_FAILED로 바꾼다. 해제 통지가 늦으면 만료 작업과 주기 대조가 재고를 회수한다. - 확정 주문이 출고 계획 사건을 발행한다. 알림은 그 사건을 나중에 소비하므로 알림 장애가 주문 확정을 되돌리지는 않는다.
Saga는 여러 저장소에 걸친 하나의 주문을 작은 거래의 연속으로 나누고, 중간 실패 때 이미 끝난 단계에 반대 업무를 실행하는 방법이다. 여기서 보상은 시간을 되돌리는 데이터베이스 롤백이 아니라 재고 해제나 결제 취소라는 새 동작이다. 주문 단계가 많고 고객 지원이 현재 위치를 설명해야 하므로, 각 서비스가 알아서 다음 사건을 일으키는 방식보다 상태를 내구 저장소에 기록하는 중앙 주문 조정자를 선택한다.
보낸 뒤에는 상태가 거꾸로 가지 않게: 배송과 반품 사건을 추가한다
GET /v1/orders/{order_id}
GET /v1/orders/{order_id}/shipments
POST /v1/orders/{order_id}/returns
Idempotency-Key: return:{order_id}:{client_attempt_id}
webhook은 택배사나 결제사 같은 외부 시스템이 상태 변화를 우리 주소로 보내는 요청이다. 이 글에서는 결제·배송 통지를 받는 역할을 하며, 같은 통지가 다시 오거나 순서가 뒤집힐 수 있으므로 공개 사용자 API와 분리하고 제공자 사건 ID를 고유 키로 저장해야 한다.
- 창고가 피킹, 포장, 택배사 인계 사건을 추가한다. 각 사건에는 배송 ID, 사건 ID, 발생 시각, 수신 시각, 위치와 출처를 둔다.
- 택배사 webhook이 이동 중, 배송 출발, 완료, 실패 사건을 보낸다. 서명을 검증하고
(택배사, 택배사 사건 ID)중복을 막는다. - 배송 상태 계산기가 사건을 규칙에 따라 해석해 사용자가 보는 현재 상태를 만든다. 늦게 도착한
이동 중사건은 이미 확정한배송 완료를 되돌리지 못한다. - 잘못된 사건은 삭제하지 않고 정정 또는 무효 관계를 가진 새 사건으로 남긴다. GS1의
EPCIS(Electronic Product Code Information Services)는 무엇이 언제 어디서 왜 움직였는지 사건으로 기록하는 물류 표준이며, 정정 뒤에도 추적 가능한 이력을 보존하는 근거로 사용한다. - 반품은 주문 품목별 조건을 확인한 뒤
REQUESTED → APPROVED → PICKUP_SCHEDULED → IN_RETURN_TRANSIT → RECEIVED → INSPECTED → REFUND_PENDING → REFUNDED로 진행한다. 거절·분실·부분 환불은 이유 코드와 별도 분기로 남긴다. - 검수 뒤 재판매 가능한 수량은 재고 조정 사건으로 돌려놓고, 불량품은 격리한다. 환불은 원 결제 시도와 반품 품목 금액에 연결해 중복과 초과 환불을 막는다.
세 약속을 잇는 기록은 업무 키·이전 상태·추가 이력을 강제한다
원장은 돈이나 수량의 변화를 이전 기록을 지우지 않고 차례로 남긴 기록이다. 현재 숫자만으로는 중복 차감이나 수동 보정의 이유를 찾기 어렵기 때문에 재고 조정과 결제 금액을 감사하고 복구하는 데 쓴다. 사용자 조회 때마다 처음부터 합산하지는 않고, 같은 거래로 갱신하거나 사건에서 만든 현재 잔액 사본을 읽는다.
| 영역 | 권위 기록 | 빠른 조회용 사본 | 지켜야 할 규칙 |
|---|---|---|---|
| 상품 | 상품·SKU·판매 조건의 버전 | 검색 문서, 상세 캐시 | 결제에서 버전 재검증 |
| 재고 | 현재 수량, 예약, 조정 이력 | 지역별 가용량 | 가용량 음수 금지 |
| 주문 | 주문·품목·상태 이동 | 주문 목록과 상세 | 허용된 이전 상태에서만 이동 |
| 결제 | 결제 시도·사건·환불 | 주문별 결제 요약 | 업무 키와 제공자 ID 고유 |
| 배송 | 배송 사건 | 현재 배송 상태 | 사건 추가, 과거 상태 역전 방지 |
| 반품 | 반품·품목·상태 이동 | 반품 상세 | 주문 수량·결제액 초과 금지 |
핵심 필드는 다음처럼 잡을 수 있다.
stock_balance(sku_id, warehouse_id, on_hand, reserved, allocated, version,
PRIMARY KEY(sku_id, warehouse_id))
reservation(reservation_id PK, order_id, sku_id, warehouse_id, quantity,
status, expires_at, idempotency_key UNIQUE)
inventory_adjustment(adjustment_id PK, sku_id, warehouse_id, delta, reason,
reference_type, reference_id, occurred_at)
orders(order_id PK, user_id, status, currency, total, version,
idempotency_key UNIQUE, created_at)
order_line(order_id, line_id, sku_id, product_title_snapshot,
unit_price_snapshot, quantity, fulfillment_status)
order_transition(order_id, transition_id, from_status, to_status,
reason, event_id UNIQUE, occurred_at)
payment_attempt(payment_attempt_id PK, order_id, provider,
provider_payment_id UNIQUE, idempotency_key UNIQUE,
amount, status, created_at)
refund(refund_id PK, return_id, payment_attempt_id, amount,
provider_refund_id UNIQUE, idempotency_key UNIQUE, status)
shipment_event(shipment_id, event_id, carrier_event_id, type,
occurred_at, received_at, source, correction_of,
PRIMARY KEY(shipment_id, event_id),
UNIQUE(source, carrier_event_id))
return_request(return_id PK, order_id, status, idempotency_key UNIQUE)
return_line(return_id, order_line_id, quantity, inspection_result, refund_amount)
주문 목록은 (user_id, created_at DESC, order_id)로 페이지를 넘기고, 예약 만료는 (status, expires_at)로 찾는다. 상태 이동은 ID뿐 아니라 현재 버전과 이전 상태가 예상과 같은지도 조건으로 검사한다. 갱신에 실패하면 최신 상태를 읽어 이미 처리한 중복인지 서로 다른 변경의 충돌인지 구분한다.
TTL(Time To Live)은 저장 항목을 유지하는 기간이다. 멱등 키의 TTL은 업무별 재시도와 분쟁 기간보다 길게 둬야 하지만, 정확한 기간은 사업 규칙과 결제 계약으로 정한다. TTL 만료 뒤에도 주문과 결제 자체의 고유 ID는 다시 쓰이지 않게 해야 한다.
복잡한 분기는 아래 상태도에서 위쪽 주문 흐름을 먼저 왼쪽에서 오른쪽으로 읽고, 실패 화살표가 어떤 보상으로 이어지는지 본다. 그다음 배송 완료 이후 아래쪽 반품 흐름이 새 업무로 시작되는 지점을 확인한다.

상태는 마지막 값으로 무조건 덮어쓰지 않는다. 고유 사건 ID, 허용된 이전 상태와 버전을 함께 검사하고, 이미 출고된 주문처럼 되돌릴 수 없는 단계는 취소가 아니라 반품·환불이라는 다음 흐름으로 넘긴다.
저장 성공과 사건 발행 사이의 틈은 중복을 허용해 메운다
transactional outbox(트랜잭셔널 아웃박스)는 주문 행과 나중에 보낼 사건을 같은 로컬 데이터베이스 거래에 저장한 뒤 별도 전달기가 메시지 전달 시스템으로 옮기는 방법이다. 이 글에서는 주문 변경과 발행할 사건을 함께 보존하며, 주문 저장은 성공했지만 사건 발행만 빠지는 틈을 막기 위해 필요하다.
전달기는 실패 시 다시 보내므로 같은 사건이 여러 번 올 수 있다. 소비자는 event_id 처리 기록과 현재 업무 상태를 함께 확인한다. Apache Kafka 공식 문서도 메시지를 잃지 않기 위해 중복 전달을 허용하는 방식에서는 처리 후 읽은 위치를 저장하기 전에 소비자가 죽으면 같은 메시지를 다시 받을 수 있다고 설명한다. Kafka 내부의 정확히 한 번 처리 범위를 외부 결제사나 별도 데이터베이스까지 자동으로 넓혀 해석해서는 안 된다.
순서가 필요한 단위가 메시지 저장 구역의 키가 된다. 주문은 order_id, 배송은 shipment_id, 일반 재고는 (sku_id, warehouse_id)로 묶는다. 다만 핫딜 한 상품은 그 키 하나에 요청이 몰리므로 별도 제어가 필요하다.
핫딜은 캐시보다 한 재고 키의 판정 속도가 먼저 막힌다
사용자가 가장 먼저 보는 증상은 결제 대기 끝의 품절 또는 주문 뒤 취소다. 데이터베이스 전체가 한가해도 (SKU, 창고) 한 행에 조건부 쓰기가 몰리면 그 상품만 멈춘다. SKU별 예약 시도와 성공 비율, 갱신 충돌률, 키별 대기 시간, 품절 뒤 유입률과 예약 만료율로 이를 감지한다.
완화는 다음 순서로 적용한다.
- 판매 전에 계정·기기·상품별 구매 수량과 요청 횟수를 제한한다.
- 입구에서 실제 판매 속도에 맞춰 입장 표를 발급하고 대기 중·품절을 즉시 보여준다.
- 재고가 작으면 SKU별 단일 처리자가 예약 명령을 차례로 판정한다. 정확한 순서는 쉽지만 그 처리자의 속도가 상한이다.
- 재고가 크면 수량을 여러 묶음으로 미리 나눠 요청을 분산한다. 처리량은 늘지만 한 묶음은 품절이고 다른 묶음은 남는 불균형과 재조정 비용이 생긴다.
- 캐시의 예상 수량이 0이면 빠르게 품절을 알리되, 캐시를 최종 판매 판정자로 쓰지 않는다.
예약 만료도 쓰기 폭주를 만든다. 모든 핫딜 예약에 같은 10분 만료를 주면 10분 뒤 해제 요청이 한꺼번에 온다. 만료 시각 구간을 나눠 작은 배치로 조건부 해제하고, 결제 인증 중인 예약의 연장 횟수에는 상한을 둔다. 만료 작업이 멈추면 예약 상태인데 만료 시각이 지난 수량, 가용량 대조 차이와 오래된 주문을 경보로 삼아 다시 실행한다.
세 약속이 깨질 때 사용자가 보는 현상부터 복구한다
| 사용자가 보는 현상 | 감지 | 즉시 완화 | 복구와 정합성 확인 |
|---|---|---|---|
| 검색에는 재고가 있지만 결제 때 품절 | 색인 지연, 예약 실패율 | 결제 직전 재검증 후 즉시 품절 반환 | 검색 문서 버전 갱신, 결제됐다면 취소·환불 대조 |
결제 뒤 오래 처리 중 |
주문 단계별 체류 시간, 결제사 시간 초과 | 새 결제를 만들지 않고 주문 ID로 조회 제공 | 같은 멱등 키로 조회·재시도, 결제사 사건과 내부 시도 대조 |
| 주문이나 알림이 두 번 보임 | 멱등 키 충돌, 사건 ID 중복률 | 기존 결과 반환, 알림 소비 중복 제거 | 주문·결제·알림 고유 키 대조와 중복 부작용 취소 |
| 돈은 빠졌는데 주문 실패로 보임 | 결제 성공 통지와 주문 상태 불일치 | 확인 중 상태와 문의 기준 표시 |
주문 확정 또는 정책상 불가능하면 환불, 금액 원장과 결제사 명세 대조 |
| 배송 완료 뒤 다시 이동 중 | 이전 상태로의 이동 거부 수 | 늦은 사건은 이력에만 표시 | 발생·수신 시각과 정정 관계로 현재 상태 재생성 |
| 반품 회수 뒤 환불이 멈춤 | 상태별 체류 시간, 회수·입고 불일치 | 예상 처리일과 현재 단계 노출, 지원 작업 생성 | 택배·검수·환불 사건 대조 후 재시도 또는 감사 기록을 남긴 수동 판정 |
재시도에는 지수 backoff를 쓴다. 실패 직후 계속 호출하지 않고 재시도 간격을 점점 늘리는 방법으로, 장애 난 결제사나 저장소를 더 압박하지 않기 위해 필요하다. 여기에 임의의 짧은 지연을 더해 모든 서버가 동시에 재시도하는 상황도 피한다.
외부 webhook은 서명 확인과 내구 저장을 마치면 빠르게 성공 응답을 돌려주고 실제 주문 변경은 뒤에서 처리한다. Stripe 공식 문서처럼 자동 재시도와 수동 재전송이 함께 가능한 경우가 있으므로 제공자 사건 ID의 중복 제거는 필수다.
세 약속이 충돌할 때 보안과 비용의 경계를 고른다
주문·배송·반품 조회는 로그인만 확인해서는 부족하다. 요청한 사용자가 그 주문 ID를 볼 권한이 있는지 매번 검사한다. 관리자용 가격·재고·환불 API는 구매 API와 권한을 분리하고 기본 거부하며, 고객 지원의 조회와 수동 변경에는 감사 기록을 남긴다.
카드 번호는 직접 저장하지 않고 결제 제공자의 토큰을 사용한다. PCI DSS(Payment Card Industry Data Security Standard)는 카드 데이터 보호를 위한 보안 표준이며, 이 설계의 정확한 적용 범위는 결제 통합 방식과 계약에 따라 따로 평가해야 한다. 주소와 전화번호는 암호화하고 역할과 업무 목적에 따라 조회를 좁히며, 로그와 메시지에는 전체 주소나 결제 토큰을 복사하지 않는다.
비용은 10억 회 읽기의 네트워크·캐시·검색, 사건 장기 보존과 여러 지역 복제에서 커진다. CDN(Content Delivery Network)은 사용자 가까이에 이미지 같은 정적 파일 사본을 두는 전달망이며, 원본 부하와 전송 지연을 줄이는 데 쓴다. 검색의 모든 변경에 강제 갱신을 걸지 않고, 즉시 판매 중단이 필요한 상품만 작은 차단 목록으로 먼저 막는다.
최근 운영 조회와 재처리에 필요한 사건은 빠른 저장소에 두고, 규정·감사용 장기 데이터는 암호화한 저비용 저장소로 옮긴다. 검색·상품 읽기는 여러 지역으로 복제할 수 있지만 재고·주문 쓰기는 상품이나 지역별 주 소유자를 먼저 정한다. 처음부터 모든 지역에서 동시에 쓰게 하면 재고 충돌과 운영비가 크게 늘어난다.
선택하지 않은 대안은 실패 형태와 운영 부담이 다르다
| 결정 | 선택안 | 대안과 장단점 |
|---|---|---|
| 초기 거래 구조 | 한 관계형 데이터베이스의 모듈형 시스템 | 구현·관측이 단순하다. 외부 결제·택배까지 한 거래로 묶을 수 없고 독립 확장에는 한계가 있다. |
| 영역 분리 뒤 주문 | 상태를 저장하는 중앙 Saga 조정자 | 실패 단계가 보이지만 보상·중복·운영 도구가 필요하다. 참여자가 적을 때의 자율 사건 방식은 결합이 낮지만 단계가 늘면 의존성이 숨는다. |
| 재고 예약 | DB 조건부 갱신 | 정확성을 설명하기 쉽지만 핫딜 키의 쓰기 상한이 낮다. 순서 처리 큐는 판정이 명확하지만 대기와 복구가 필요하고, 재고 묶음 분할은 빠르지만 불균형이 생긴다. |
| 결제 순서 | 짧은 재고 예약 뒤 결제 승인 | 품절 결제를 줄이지만 인증 중 재고가 묶인다. 결제 뒤 예약은 잠금 시간을 줄이는 대신 품절 승인 취소와 수수료 부담이 생긴다. |
| 검색 최신성 | 검색 문서에 표시 필드 복제, 결제에서 원본 재검증 | 빠르고 장애를 격리하지만 잠깐 오래될 수 있다. 후보마다 원본을 읽으면 최신이지만 fan-out과 지연이 커진다. |
캐시에서 먼저 수량을 빼고 나중에 데이터베이스를 확정하는 방식은 빠르지만 둘 사이의 실패를 복구하기 어렵다. 대조 가능한 예약 ID 없이 캐시를 유일한 원장으로 삼는다면 선택하지 않는다. 결제 승인·매입·취소의 실제 순서는 결제사와 사업 정책에 맞춰 다시 정해야 한다.
단계별 확장은 실제로 막힌 사용자 약속부터 연다
- 모듈형 단일 시스템: 상품·재고·주문을 관계형 데이터베이스의 명확한 표와 거래로 시작한다. 외부 결제 멱등 키, 주문 상태 이력과 outbox는 처음부터 둔다.
- 읽기 분리: 상품 상세 캐시와 검색 색인을 떼고 변경 사건으로 갱신한다. 색인 지연을 측정하고 결제 재검증을 넣되 주문 데이터베이스는 하나로 유지한다.
- 거래 영역 분리: 팀·부하·장애 요구가 실제로 달라질 때 재고, 주문과 결제 연결부를 나눈다. Saga 조정자, 소비자 중복 제거, 대조 작업과 운영 화면을 함께 만든다.
- 핫딜 전용 경로: 상품별 입장 제어와 수량 제한, 순서 처리 또는 재고 묶음 분할을 적용한다. 일반 트래픽과 격리하고 부하 시험으로 한 키의 상한을 측정한다.
- 지역·물류 확장: 읽기는 여러 지역으로 복제하고 재고 쓰기의 상품·창고별 주 소유자를 정한다. 택배 사건 정규화, 역전·정정 규칙과 반품·환불 대조를 강화한다.
전환 신호는 마이크로서비스가 적어 보인다는 느낌이 아니라 데이터베이스 경합, 팀의 배포 충돌, 장애 전파, 핫키 대기, 색인 지연과 복구 시간처럼 측정 가능한 한계여야 한다.
선택과 아직 정하지 못한 처리 시간
선택은 분명하다. 검색의 빠른 안내와 결제의 정확한 판정을 같은 데이터에 맡기지 않는다. 결제는 짧은 재고 예약과 중앙 주문 조정자로 수렴시키고, 배송과 반품은 사건을 추가해 늦은 통지가 현재 상태를 거꾸로 돌리지 못하게 한다.
아직 숫자로 정할 수 없는 시간도 있다. 결제 승인과 재고 확정 사이가 불명확할 때 고객에게 PROCESSING을 얼마나 오래 보여줄지는 사업 정책, 결제사 계약과 실제 지연 분포가 없으면 결정할 수 없다. 쿠팡의 내부 운영 수치와 택배사별 사건 품질도 확인하지 않았으므로, 재고 예약 시간·개인정보 보존 기간·승인과 매입 순서는 부하 시험과 법률·계약 검토 뒤에 정해야 한다.
참고 링크
- OpenSearch Refresh Index API
- AWS Saga patterns
- Stripe Idempotent requests
- Apache Kafka Message Delivery Semantics
- GS1 EPCIS 2.0
- OWASP API Security Project
- PCI Security Standards Council
구현 전에는 위 공식 자료에서 색인 갱신, 결제 재시도, Saga와 물류 사건의 보장 범위를 사용 중인 제품과 계약 조건에 맞춰 다시 확인한다.