Files
2nd/10_Wiki/Topics/Architecture/이커머스의 실시간 재고 관리.md
Antigravity Agent f8b21af4be Wiki cleanup: error-doc removal, dedup merge, link normalization
10_Wiki/Topics 대규모 정리:
- 오류 캡처/미완성 stub 문서 227개 제거
- 교차폴더 중복 43클러스터 병합 (63파일 → redirect)
- 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건
- 카테고리 MOC 6개 신규 생성
- Graph 섹션 미해결 related-keyword 링크 10,058건 제거

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:52:15 +09:00

6.5 KiB
Raw Permalink Blame History

id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
id title category status canonical_id aliases duplicate_of source_trust_level confidence_score verification_status tags raw_sources last_reinforced github_commit tech_stack
wiki-2026-0508-이커머스의-실시간-재고-관리 이커머스의 실시간 재고 관리 10_Wiki/Topics verified self
Real-time Inventory
Stock Management
Inventory Reservation
none A 0.9 applied
ecommerce
inventory
event-driven
distributed-systems
2026-05-10 pending
language framework
typescript redis-kafka

이커머스의 실시간 재고 관리

매 한 줄

"매 stock = 단일 진실 + reservation log". 이커머스 재고는 oversell·undersell 사이의 trade-off — 동일 SKU에 동시 구매 요청이 몰리면 절대 수량은 atomic하게 감소해야 하고, 결제 실패 시 복구도 atomic해야 한다. 2026 표준은 Redis(또는 DB row lock)에서 reservation, Kafka로 event publish, eventual consistency로 warehouse·search·analytics 동기화.

매 핵심

매 두 모델

  • Reserve-then-confirm (most): cart 추가 시 hold (TTL 10-30분), 결제 완료 시 commit. Hold 만료 → 자동 release.
  • Optimistic deduction: 결제 시점에만 차감 — 빠르지만 oversell 위험. 재고 풍부 시 OK.

매 일관성 layer

  • Source of truth: 단일 store (Postgres row 또는 Redis HSET). Strict serializable.
  • Read replica / search index: eventual consistency. Kafka changelog로 동기화.
  • CDN cached PDP: stale 허용 (몇 초). "Add to cart" 시 source 재확인.

매 응용

  1. Flash sale: pre-allocate per-shard inventory, hot-key 분산.
  2. Multi-warehouse: SKU × warehouse 행렬, 배송 가능성으로 reservation routing.
  3. Marketplace: 판매자 자체 stock + platform reservation.

💻 패턴

Redis Lua로 atomic reserve

-- reserve.lua: KEYS[1]=stock:{sku}, ARGV[1]=qty, ARGV[2]=reservationId, ARGV[3]=ttlSec
local available = tonumber(redis.call('GET', KEYS[1]) or '0')
local qty = tonumber(ARGV[1])
if available < qty then return {0, available} end
redis.call('DECRBY', KEYS[1], qty)
redis.call('SET', 'reserve:' .. ARGV[2], qty, 'EX', ARGV[3])
return {1, available - qty}
import { createClient } from 'redis';
const redis = createClient(); await redis.connect();
const reserveScript = await redis.scriptLoad(LUA);

export async function reserve(sku: string, qty: number, ttlSec = 1800) {
  const reservationId = crypto.randomUUID();
  const [ok, remaining] = await redis.evalSha(reserveScript, {
    keys: [`stock:${sku}`],
    arguments: [String(qty), reservationId, String(ttlSec)],
  }) as [number, number];
  if (!ok) throw new OutOfStockError(sku, qty, remaining);
  return reservationId;
}

Postgres row-level lock 대안

-- single SKU, strong consistency, ~1k tx/s 까지 OK
BEGIN;
SELECT available FROM stock WHERE sku = $1 FOR UPDATE;
-- application-side check
UPDATE stock SET available = available - $2 WHERE sku = $1 AND available >= $2;
INSERT INTO reservation (id, sku, qty, expires_at)
  VALUES ($3, $1, $2, now() + interval '30 minutes');
COMMIT;

Confirm / Release

export async function confirm(reservationId: string) {
  const r = await db.reservation.findUnique({ where: { id: reservationId } });
  if (!r || r.status !== 'held') throw new Error('invalid reservation');
  await db.$transaction([
    db.reservation.update({ where: { id: reservationId }, data: { status: 'confirmed' } }),
    db.stockMovement.create({ data: { sku: r.sku, qty: -r.qty, kind: 'sale' } }),
  ]);
  await kafka.publish('inventory.events', {
    type: 'StockSold', sku: r.sku, qty: r.qty, reservationId,
  });
}

export async function release(reservationId: string) {
  // Lua에서 stock:{sku} INCRBY qty + DEL reserve:{id}
}

TTL 만료 자동 release (Redis keyspace notifications)

// redis CONFIG SET notify-keyspace-events Ex
const sub = redis.duplicate(); await sub.connect();
await sub.pSubscribe('__keyevent@0__:expired', async (key) => {
  if (key.startsWith('reserve:')) {
    const id = key.slice('reserve:'.length);
    await release(id);   // idempotent
  }
});

Kafka changelog → search/analytics

// consumer: inventory.events → ElasticSearch
const consumer = kafka.consumer({ groupId: 'search-indexer' });
await consumer.subscribe({ topic: 'inventory.events' });
await consumer.run({
  eachMessage: async ({ message }) => {
    const e = JSON.parse(message.value!.toString());
    if (e.type === 'StockSold' || e.type === 'StockAdded') {
      const remaining = await redis.get(`stock:${e.sku}`);
      await es.update({
        index: 'products', id: e.sku,
        doc: { in_stock: Number(remaining) > 0, available: Number(remaining) },
      });
    }
  },
});

Hot-key 분산 (flash sale)

// SKU 'AIR-MAX' 10000개 → 10 shard 각 1000개
const shards = 10;
async function reserveSharded(sku: string, qty: number) {
  const shardOrder = shuffle([...Array(shards).keys()]);
  for (const i of shardOrder) {
    try { return await reserve(`${sku}:shard:${i}`, qty); }
    catch (e) { if (!(e instanceof OutOfStockError)) throw e; }
  }
  throw new OutOfStockError(sku, qty, 0);
}

매 결정 기준

상황 Approach
재고 풍부, 동시성 낮음 Postgres row lock
동시성 높음, 단일 SKU Redis Lua + reservation TTL
Flash sale, 동일 SKU 폭주 Sharded inventory
Multi-warehouse SKU × WH 매트릭스 + routing

기본값: Redis reservation + Kafka changelog. Postgres는 audit·reconciliation용 source of truth.

🔗 Graph

🤖 LLM 활용

언제: reservation 흐름 설계, oversell incident 분석, hot-key 식별. 언제 X: 재고 추적이 필요 없는 digital good (license key 무한) — 단순 카운터로 충분.

안티패턴

  • READ + 그 다음 UPDATE (lock 없이): TOCTOU race → oversell.
  • Reservation 만료 처리 누락: hold이 영원히 남아 가용 재고 잠식.
  • Search index를 source of truth로 사용: ES eventual consistency → 결제 시점 negative stock.
  • 재고 0에서 cart 추가 허용: UX는 좋지만 결제 실패 폭주.

🧪 검증 / 중복

  • Verified (Shopify Engineering, "Building Resilient Payment Systems", 2024; AWS retail patterns).
  • 신뢰도 A.

🕓 Changelog

날짜 변경
2026-05-08 Phase 1
2026-05-10 Manual cleanup — reservation 모델·Lua·sharding 추가