136 lines
5.3 KiB
Markdown
136 lines
5.3 KiB
Markdown
---
|
|
id: idempotent-operations
|
|
title: 멱등성 연산 실전 (Idempotent Operations)
|
|
category: Coding
|
|
status: draft
|
|
canonical_id: idempotent-operations
|
|
aliases: [idempotency, retry-safe, exactly-once illusion, 멱등성]
|
|
duplicate_of: null
|
|
source_trust_level: B
|
|
confidence_score: 0.9
|
|
verification_status: conceptual
|
|
created_at: 2026-05-09
|
|
updated_at: 2026-05-09
|
|
last_reinforced: 2026-05-09
|
|
review_reason: ""
|
|
merge_history: []
|
|
tags: [coding, distributed, http, message-queue, retry, vibe-coding]
|
|
raw_sources: ["P-Reinforce session 2026-05-09 — bulk Coding seed batch 1"]
|
|
tech_stack:
|
|
language: "TypeScript / SQL / HTTP"
|
|
applicable_to: ["Backend", "Worker", "API design"]
|
|
applied_in: []
|
|
---
|
|
|
|
# 멱등성 연산 실전
|
|
|
|
> 같은 요청이 N번 와도 부작용은 1번분만. 분산 시스템에서 "exactly-once" 는 불가능에 가깝고, "at-least-once + idempotent" 가 현실적 답이다.
|
|
|
|
## 📖 핵심 개념
|
|
|
|
**멱등성(idempotent)**: `f(f(x)) == f(x)`. 연산을 여러 번 적용해도 첫 번째 적용과 결과가 같음.
|
|
|
|
분산 환경의 모든 네트워크 호출은 **재시도 가능성**이 있다 (ACK 유실 / 타임아웃 / 클라이언트 retry). 따라서 다음을 항상 가정:
|
|
- 클라이언트는 같은 요청을 여러 번 보낼 수 있다
|
|
- 메시지 큐는 같은 메시지를 여러 번 전달할 수 있다 (at-least-once delivery)
|
|
- DB 트랜잭션은 commit 후 응답을 못 받고 재시도될 수 있다
|
|
|
|
해결: **연산 자체를 멱등하게** 만들거나, **외부 idempotency key**로 중복 감지.
|
|
|
|
## 💻 코드 패턴
|
|
|
|
### 1. HTTP 엔드포인트 — Idempotency-Key 헤더
|
|
|
|
```ts
|
|
// Express
|
|
app.post('/api/payments', async (req, res) => {
|
|
const key = req.header('Idempotency-Key');
|
|
if (!key) return res.status(400).json({ error: 'Idempotency-Key required' });
|
|
|
|
// 1) 같은 키의 이전 응답을 캐시에서 조회
|
|
const cached = await redis.get(`idem:${key}`);
|
|
if (cached) return res.json(JSON.parse(cached));
|
|
|
|
// 2) 진짜 결제 수행 (DB 트랜잭션 안에서 키 unique 보장)
|
|
try {
|
|
const payment = await db.transaction(async (tx) => {
|
|
await tx.idempotencyKeys.insert({ key, status: 'processing' }); // UNIQUE
|
|
const result = await chargePayment(req.body);
|
|
await tx.idempotencyKeys.update(key, { status: 'done', response: result });
|
|
return result;
|
|
});
|
|
await redis.setex(`idem:${key}`, 86400, JSON.stringify(payment));
|
|
res.json(payment);
|
|
} catch (e) {
|
|
if (isUniqueViolation(e)) {
|
|
// 이전 호출이 진행 중 — 클라이언트가 잠시 후 재시도하도록
|
|
return res.status(409).json({ error: 'In progress, retry later' });
|
|
}
|
|
throw e;
|
|
}
|
|
});
|
|
```
|
|
|
|
### 2. SQL — UPSERT 로 멱등 INSERT
|
|
|
|
```sql
|
|
-- 같은 user_id 가 여러 번 들어와도 한 행
|
|
INSERT INTO user_settings (user_id, theme)
|
|
VALUES ($1, $2)
|
|
ON CONFLICT (user_id) DO UPDATE SET theme = EXCLUDED.theme;
|
|
```
|
|
|
|
### 3. Message Queue Consumer — processed_id 테이블
|
|
|
|
```ts
|
|
async function handleMessage(msg: { id: string; payload: any }) {
|
|
const inserted = await db.processedMessages.insertIfAbsent({ id: msg.id }); // UNIQUE
|
|
if (!inserted) {
|
|
logger.info('Duplicate message ignored', { id: msg.id });
|
|
return; // 이미 처리됨
|
|
}
|
|
await actuallyProcess(msg.payload);
|
|
}
|
|
```
|
|
|
|
## 🤔 의사결정 기준
|
|
|
|
| HTTP 메서드 | 멱등성 기본값 | 강제 필요 |
|
|
|---|---|---|
|
|
| GET / HEAD / PUT / DELETE | 멱등 (RFC 9110) | 일반적으로 자연스러움 |
|
|
| POST | 비멱등 | **반드시 Idempotency-Key 도입** |
|
|
|
|
| 작업 종류 | 멱등화 전략 |
|
|
|---|---|
|
|
| 결제 / 외부 API 호출 | Idempotency-Key + 응답 캐시 |
|
|
| DB INSERT | UPSERT or UNIQUE 제약 |
|
|
| 메시지 큐 consumer | processed_id 테이블 |
|
|
| 파일 업로드 | content hash 기반 dedup |
|
|
| 카운터 증가 | **이건 멱등화 어려움** — sequence number / event sourcing 고려 |
|
|
|
|
## ❌ 안티패턴
|
|
|
|
- **클라이언트가 키를 안 보냈다고 그냥 처리**: "키 없으면 실패" 가 안전. 키를 매번 새로 만드는 클라이언트는 매번 새 결제로 처리되어도 클라이언트 책임.
|
|
- **응답 캐시 TTL 너무 짧음**: 24시간 권장. 그보다 짧으면 늦은 retry가 새 처리를 만듦.
|
|
- **카운터 증가를 "그냥 두 번 올리면 되지" 식으로 처리**: at-least-once + ++counter 는 정확한 카운트 보장 못 함. 이벤트 소싱 또는 외부 dedup 필요.
|
|
- **트랜잭션 밖에서 키 체크**: 키 체크 → 실 처리 사이에 race. 키 INSERT는 같은 트랜잭션 안에서.
|
|
- **에러 응답도 캐싱**: 일시 오류(5xx)를 캐시하면 후속 retry가 모두 실패. 4xx만 캐싱, 5xx는 캐시 안 함.
|
|
|
|
## 🤖 LLM 활용 힌트
|
|
|
|
- LLM에게 외부 호출 코드 작성을 시킬 때 "**at-least-once delivery 가정 + Idempotency-Key 패턴 적용**" 명시.
|
|
- DB INSERT 코드: "**같은 키로 N번 호출되어도 한 행만 남도록**" 라고 요청 → UPSERT or UNIQUE.
|
|
- 메시지 핸들러 작성: "**이 핸들러는 같은 메시지가 N번 들어와도 안전해야 함**" 명시.
|
|
|
|
## 🧪 검증 상태
|
|
|
|
- verification_status: `conceptual`
|
|
- Stripe / AWS / Square 의 실제 결제 API 가 모두 Idempotency-Key 사용 → B급 출처.
|
|
- 적용 사례 발견 시 `applied_in` 추가.
|
|
|
|
## 🔗 관련 문서
|
|
|
|
- [[Optimistic_Concurrency_Control]]
|
|
- [[Backpressure_Patterns]]
|
|
- [[Error_Handling_Result_vs_Throw]]
|