[G1-Sync] Manual knowledge update

This commit is contained in:
Antigravity Agent
2026-05-10 22:08:15 +09:00
parent 21ac3ed255
commit 504fd5fb42
3011 changed files with 380280 additions and 206977 deletions
@@ -0,0 +1,387 @@
---
id: backend-idempotency-deep
title: Idempotency Deep — keys / windows / storage
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [backend, idempotency, vibe-coding]
tech_stack: { language: "TS", applicable_to: ["Backend"] }
applied_in: []
aliases: [idempotency key, deduplication, exactly-once, retry-safe, request-id]
---
# Idempotency Deep
> Network / retry = 같은 request 가 여러 번. **Idempotency key 가 dedupe**. Stripe / 모든 payment API 의 표준.
## 📖 핵심 개념
- 같은 key + 같은 request = 같은 result (1번 effect).
- Storage 가 매 key 의 result.
- TTL (24-48 hr).
- Concurrency 안전.
## 💻 코드 패턴
### 기본 (단순)
```ts
async function createCharge(req) {
const key = req.headers['idempotency-key'];
if (!key) return { error: 'missing key' };
const cached = await db.idempotency.findOne({ key });
if (cached) return cached.response;
const result = await stripe.charges.create(req.body);
await db.idempotency.insert({ key, response: result, created_at: new Date() });
return result;
}
```
→ Race condition: 두 request 가 동시 = 2번 호출 가능.
### Concurrency-safe (lock)
```ts
async function createCharge(req) {
const key = req.headers['idempotency-key'];
// 1. Try insert (atomic)
try {
await db.idempotency.insert({
key, status: 'in_progress', created_at: new Date()
});
} catch (e) {
if (isUniqueViolation(e)) {
// Existing → return cached or wait
const cached = await db.idempotency.findOne({ key });
if (cached.status === 'completed') return cached.response;
if (cached.status === 'in_progress') {
// Wait or 409
return { error: 'in progress' }; // 409 Conflict
}
}
throw e;
}
// 2. Process
try {
const result = await stripe.charges.create(req.body);
await db.idempotency.update({ key }, { status: 'completed', response: result });
return result;
} catch (e) {
await db.idempotency.update({ key }, { status: 'failed', error: e.message });
throw e;
}
}
```
### Postgres unique constraint
```sql
CREATE TABLE idempotency_keys (
key TEXT PRIMARY KEY,
status TEXT NOT NULL,
response JSONB,
request_hash TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL
);
```
→ INSERT 가 atomic. Same key 두 번 = unique violation.
### Request hash (validation)
```ts
async function createCharge(req) {
const key = req.headers['idempotency-key'];
const reqHash = sha256(JSON.stringify(req.body));
const cached = await db.idempotency.findOne({ key });
if (cached) {
if (cached.request_hash !== reqHash) {
return { error: 'idempotency key reused with different body' }; // 422
}
return cached.response;
}
// ... 처리
await db.idempotency.insert({ key, request_hash: reqHash, ... });
}
```
→ "Same key 다른 body" = 클라이언트 bug.
### Stripe 식
```ts
// Stripe 가 client 의 패턴
const charge = await stripe.charges.create(
{ amount: 1000, currency: 'usd', source: 'tok_x' },
{ idempotencyKey: 'order_123' }
);
```
→ Stripe 가 24 hr 안 같은 result.
### TTL
```sql
-- 24-48 hr 후 삭제
DELETE FROM idempotency_keys WHERE expires_at < NOW();
-- 또는 partition
CREATE TABLE idempotency_keys_2026_05_09 PARTITION OF idempotency_keys
FOR VALUES FROM ('2026-05-09') TO ('2026-05-10');
```
→ 매일 partition + drop.
### Redis 식
```ts
async function createCharge(req) {
const key = `idem:${req.headers['idempotency-key']}`;
// SETNX (atomic)
const set = await redis.set(key, 'pending', 'EX', 86400, 'NX');
if (set === null) {
// Already exists
const cached = await redis.get(key);
if (cached === 'pending') return { error: 'in progress' };
return JSON.parse(cached);
}
const result = await stripe.charges.create(req.body);
await redis.set(key, JSON.stringify(result), 'EX', 86400);
return result;
}
```
→ Redis = 빠름. Persistence X (RDB / AOF 가능).
### Locking (advanced)
```ts
import { Redlock } from 'redlock';
async function createCharge(req) {
const key = req.headers['idempotency-key'];
const lock = await redlock.acquire([`lock:${key}`], 10000);
try {
// Check cache → process → save
} finally {
await lock.release();
}
}
```
→ Distributed lock 가 보장.
### HTTP semantics
```
GET, HEAD, PUT, DELETE: idempotent (HTTP spec).
POST: NOT idempotent (default).
→ POST 의 idempotency = key 명시.
PUT 도 idempotency key 가 안전.
```
### Status code
```
200/201: 첫 호출 (success).
200/201: 두 번째 (cached, same response).
422: same key, different body.
409: in progress.
410: expired key.
```
### Async / queue idempotency
```ts
// Outbox pattern
BEGIN;
INSERT INTO outbox (id, event_type, payload) VALUES ('uuid-1', 'order.placed', ...);
// duplicate id = unique violation
COMMIT;
// Consumer
async function processEvent(event) {
if (await db.processed_events.exists(event.id)) return;
// ...
await db.processed_events.insert({ id: event.id });
}
```
→ Outbox + processed events = exactly-once.
→ [[Backend_Outbox_Pattern]].
### Kafka (offset 기반)
```
Kafka consumer:
- Offset commit = "처리됨".
- Crash 후 = 옛 offset 부터 다시 → 중복 가능.
→ Idempotent processing (DB upsert + dedup).
```
### Webhook idempotency
```ts
// Webhook receiver
async function handleWebhook(req) {
const eventId = req.body.id;
if (await db.webhook_events.exists(eventId)) {
return res.status(200).json({ received: true });
}
await db.webhook_events.insert({ id: eventId });
await processWebhook(req.body);
res.status(200).json({ received: true });
}
```
→ Stripe / GitHub 가 retry — duplicate event 가능.
### Race in receiver
```ts
// 두 webhook 가 동시 도착
// transaction 안에서 insert + process
BEGIN;
INSERT INTO webhook_events (id) VALUES ($1); -- unique
// process
COMMIT;
```
### Multi-step (saga)
```
매 step 의 idempotency.
- reserveItems(itemIds, idempotencyKey): 같은 key = 같은 reservation.
- chargePayment(amount, idempotencyKey): 같은 key = 1번 charge.
- shipOrder(orderId, idempotencyKey): 같은 key = 1번 ship.
```
→ Saga retry 가 안전.
### Generated key
```ts
// Server side
const key = `${userId}:${orderId}:${timestamp}`;
// Client side (UUID)
const key = crypto.randomUUID();
```
→ Client 가 generate (Stripe 식).
### Storage capacity
```
1M req/day × 2 KB / req = 2 GB / day.
24 hr TTL = 2 GB live.
→ 큰 system 가 partition / TTL.
```
### Performance 영향
```
매 request 가 1 DB round-trip (idempotency check).
Cache (Redis) 가 mitigate.
Hot key 가 hot row.
→ Production 가 Redis.
```
### Frontend (UI)
```ts
// Generate key once per "submit"
const idempKey = useRef(crypto.randomUUID());
const handleSubmit = async () => {
await fetch('/charge', {
method: 'POST',
headers: { 'idempotency-key': idempKey.current },
body: JSON.stringify(data),
});
};
```
→ Retry on network error 가 안전.
### Failure 시 cleanup
```ts
try {
// process
} catch (e) {
// Mark failed (cache 가 안 stale).
await db.idempotency.delete({ key });
// 또는
await db.idempotency.update({ key }, { status: 'failed', expires_at: now + 5min });
throw e;
}
```
→ Failed 도 some retention (재시도 가 같은 fail).
### LLM call (idempotency)
```ts
// LLM 가 같은 prompt = 같은 답 안 (random).
// Determinism: temperature=0.
const cacheKey = sha256(prompt);
const cached = await cache.get(cacheKey);
if (cached) return cached;
const r = await llm.complete(prompt, { temperature: 0 });
await cache.set(cacheKey, r);
return r;
```
→ LLM cache 도 idempotency 식.
### 함정
```
- TTL 너무 짧음 (1 hr): 늦은 retry 가 missed dedup.
- TTL 너무 길음: storage 폭발.
- Key 가 짧음 (timestamp 만): 충돌.
- Process 후 store (race): 같은 key 가 여러 번.
- Body hash 안: 클라이언트 가 wrong body.
- Failed 가 cache 안 cleanup: 재시도 가 fail.
```
### 표준 (다른 vendor)
```
Stripe: 24 hr.
PayPal: 5 min.
Square: 24 hr.
GitHub API: SHA-based.
Slack: 1 hr.
```
## 🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| Payment / 외부 API | Idempotency key (Stripe 식) |
| Webhook | Event ID dedup |
| Saga step | 매 step 의 key |
| 빠른 cache | Redis SETNX |
| Persistent | Postgres unique |
| Multi-region | Distributed (Redlock 등) |
| 작은 / 단순 | DB INSERT + unique |
## ❌ 안티패턴
- **No idempotency**: retry = 중복 charge.
- **Process 후 cache**: race.
- **TTL 무한**: storage.
- **Body hash 없음**: wrong body 통과.
- **Failed 가 안 retry**: stuck.
- **Auto-generate key (timestamp)**: collision.
- **Cache only (no persistent)**: restart 후 잃음.
## 🤖 LLM 활용 힌트
- Idempotency = unique key + atomic check.
- Stripe 가 표준 (Idempotency-Key header).
- TTL 24-48 hr.
- Failed 는 짧은 cache.
## 🔗 관련 문서
- [[Backend_Idempotency_Keys]]
- [[Backend_Idempotent_Consumer]]
- [[Backend_Outbox_Pattern]]