Files
2nd/10_Wiki/Topics/Coding/Idempotent_Operations.md
T
2026-05-09 21:08:02 +09:00

5.3 KiB

id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, created_at, updated_at, last_reinforced, review_reason, merge_history, tags, raw_sources, tech_stack, applied_in
id title category status canonical_id aliases duplicate_of source_trust_level confidence_score verification_status created_at updated_at last_reinforced review_reason merge_history tags raw_sources tech_stack applied_in
idempotent-operations 멱등성 연산 실전 (Idempotent Operations) Coding draft idempotent-operations
idempotency
retry-safe
exactly-once illusion
멱등성
null B 0.9 conceptual 2026-05-09 2026-05-09 2026-05-09
coding
distributed
http
message-queue
retry
vibe-coding
P-Reinforce session 2026-05-09 — bulk Coding seed batch 1
language applicable_to
TypeScript / SQL / HTTP
Backend
Worker
API design

멱등성 연산 실전

같은 요청이 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 헤더

// 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

-- 같은 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 테이블

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 추가.

🔗 관련 문서