본문으로 건너뛰기
김신건의 로그

[Pattern] Idempotency Keys: 중복 요청 안전 처리

· 수정 · 📖 약 1분 · 439자/단어 #idempotency #pattern #api-design #backend #distributed
Idempotency Keys, Idempotency, Idempotency-Key header, Stripe idempotency, exactly-once delivery, deduplication

정의

Idempotency = 같은 요청을 N번 보내도 결과가 1번과 동일. 분산 시스템 / 결제 / API 의 안전망.

IMPORTANT

네트워크는 항상 timeout / retry. 진짜 exactly-once 는 불가능 → at-least-once + idempotent현실적 정답.

HTTP 의 idempotent method

MethodIdempotent
GETO
HEADO
OPTIONSO
PUTO
DELETEO
POSTX (기본)
PATCHX (대개)

POST 의 비-멱등성결제 중복 같은 사고의 근원.

Idempotency-Key Header

Stripe / Square / PayPal 등이 표준화:

POST /v1/charges HTTP/1.1
Authorization: Bearer sk_...
Idempotency-Key: 7f4d8e2a-c-3a-4e-9f-1234567890ab
Content-Type: application/json

{"amount": 5000, "currency": "usd", "customer": "cus_..."}

서버 응답:

1번째 호출: 200 OK (실제 처리)
2번째 호출 (같은 key): 200 OK (옛 응답 그대로)

서버 구현

sequenceDiagram
    autonumber
    participant Client
    participant API
    participant Store as Idempotency Store
    participant DB

    Client->>API: POST + Idempotency-Key: abc
    API->>Store: GET key abc
    alt 처음
        Store-->>API: not found
        API->>Store: SET key abc IN_PROGRESS
        API->>DB: 실제 처리
        DB-->>API: 결과
        API->>Store: SET key abc COMPLETED + 응답
        API-->>Client: 응답
    else 옛 응답 있음
        Store-->>API: COMPLETED + 옛 응답
        API-->>Client: 같은 옛 응답 (실제 처리 없음)
    else 진행 중
        Store-->>API: IN_PROGRESS
        API-->>Client: 409 Conflict (같은 key 중복)
    end
def idempotent_post(key, request, handler):
    cached = store.get(key)
    if cached and cached.status == "completed":
        return cached.response

    if cached and cached.status == "in_progress":
        raise Conflict("Same key in progress")

    store.set(key, "in_progress", ttl=24*3600)
    try:
        response = handler(request)
        store.set(key, "completed", response=response, ttl=24*3600)
        return response
    except Exception:
        store.delete(key)   # 또는 "failed" 로 표시
        raise

Store 선택

Store적합
Redis빠름, TTL 자동
PostgreSQL트랜잭션 친화
DynamoDB분산 + TTL

TIP

PostgreSQL 의 UNIQUE constraint + ON CONFLICT 가 가장 단순. 결제 같이 강한 일관성 필요할 때.

CREATE TABLE idempotency_keys (
  key TEXT PRIMARY KEY,
  request_hash BYTEA NOT NULL,
  response JSONB,
  status TEXT,
  expires_at TIMESTAMPTZ
);

INSERT INTO idempotency_keys (key, request_hash, status)
VALUES (?, ?, 'in_progress')
ON CONFLICT (key) DO NOTHING
RETURNING ...;

Request Hashing (재시도 안전)

def request_hash(req):
    return sha256(req.method + req.url + req.body).hexdigest()
  • 같은 key + 다른 body = 수상. 거절 (400) 또는 옛 응답.
  • 같은 key + 같은 body = 재시도. 옛 응답.

TTL 정책

기간적합
24시간Stripe 표준
7일더 안전
영구결제처럼 극한 안전

짧으면 재시도 보호 부족. 길면 store 부담.

분산 / 큐의 idempotency

flowchart LR
    P[Producer] -->|publish + msg_id| Broker
    Broker --> C[Consumer]
    C -->|이미 처리?| Inbox[(Inbox table)]
    Inbox -->|YES| Skip
    Inbox -->|NO| Process[처리 + 기록]

자세한 건 outbox-patternInbox.

멱등한 작업 만드는 패턴

작업멱등으로 만드는 방법
이메일 발송deduplication store + 발송 전 체크
DB INSERTUNIQUE constraint + ON CONFLICT
계좌 송금transaction_id 컬럼 + UNIQUE
Webhook 수신event_id + 처리 표시
API 호출Idempotency-Key 헤더

흔한 함정

WARNING

  1. 클라이언트가 매번 새 key 생성 = idempotency 의미 없음. retry 시 같은 key 재사용.
  2. key 충돌 = UUID v4 사용. 클라이언트 라이브러리가 자동 생성.
  3. TTL 너무 짧음 = 24시간 뒤 재시도중복 처리.
  4. in-progress 처리 = 그동안 동일 key 가 또 오면 대기 vs 거절 정책. 보통 409.

관련 위키

이 글의 용어 (5개)
[API Design] REST API: 원칙, 자원 모델링, HATEOAS, 버전 관리api-design
정의 REST (Representational State Transfer) 는 Roy Fielding 의 2000년 박사논문에서 정립된 분산 시스템 아키텍처 스타일. HTTP 의…
[Distributed] Kafka: 분산 로그, partition, consumer groupdistributed-systems
정의 Apache Kafka = 분산 commit log. 고처리량 (수백만 msg/s), 영속, 수평 확장. event-driven 아키텍처 의 de facto. 핵심 개념: …
[Pattern] Outbox Pattern: DB + 메시지의 원자성distributed-systems
정의 Outbox Pattern = DB 변경 + 메시지 발행 의 원자성 보장. 이중 쓰기 (dual write) 문제 의 표준 해결. 문제: Dual Write | 시나리오 |…
[Pattern] Saga: 분산 트랜잭션의 보상 흐름distributed-systems
정의 Saga = 여러 service 의 로컬 트랜잭션 을 연결한 긴 흐름. 한 단계 실패 시 이전 단계의 보상 (compensating) 트랜잭션 으로 논리적 롤백. [!IMP…
[Redis] Pub/Sub vs Streams: 휘발 신호 vs 영속 로그database-internals
정의 - Pub/Sub ( / ): 지금 듣고 있는 구독자 에게만 메시지가 전달되는 휘발성 신호. 영속 없음, ACK 없음. fan-out. - Streams ( / / / ):…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기