
AI 에이전트는 같은 결제를 아무렇지 않게 세 번이나 요청한다 - Idempotency-Key로 대응하는 RESTful API 설계
요약
AI 에이전트의 비결정성과 자동 재시도 메커니즘으로 인해 발생하는 중복 API 호출 문제를 Idempotency-Key를 통해 해결하는 RESTful API 설계 방법을 다룹니다. 결제나 발주 등 부작용이 있는 엔드포인트를 운영하는 백엔드 엔지니어를 위한 가이드입니다.
핵심 포인트
- AI 에이전트의 재시도 및 병렬 툴 호출로 인한 중복 요청 위험성
- Idempotency-Key를 활용한 서버 측 중복 실행 방지 메커니즘
- 비결정적 요청 바디에 대응하기 위한 고유 식별자 설계의 중요성
- 결제, 발주 등 Side effect가 있는 API 설계 시 필수 고려 사항
서론
상정하는 독자는 다음과 같은 분들입니다.
- LLM 에이전트로부터 호출되는 부작용(Side effect)이 있는 HTTP API를 설계·운영하고 있는 분
- 결제, 발주, 알림 전송 등, 중복 실행이 사고로 이어지는 엔드포인트를 가진 백엔드 엔지니어
- Stripe와 같은 외부 SaaS에서
Idempotency-Key를 던져본 경험은 있지만, 직접 받는 쪽을 만들어 본 적은 없는 분
이 글에서 다루지 않는 것(대상 외 독자)을 먼저 명시합니다.
- GET/HEAD와 같이 사양상 이미 멱등성(Idempotency)을 가진 메서드에 대한 이야기는 다루지 않습니다.
- 분산 트랜잭션(Saga, TCC, 2PC)의 상세 내용은 다루지 않습니다. 참조는 마지막에 조금 언급하겠습니다.
- LLM 자체의 프롬프트 설계나 에이전트 프레임워크 선정은 다루지 않습니다.
용어집
| 용어 | 정의 |
|---|---|
| Idempotency-Key | 클라이언트가 생성하는 고유 식별자. 동일한 키의 재전송에 대해 서버가 최초의 응답을 반환함 |
| ... |
전체상
이후의 장은 이 그림의 위에서 아래 순서로 대응합니다.
왜 AI 에이전트 시대에 특히 필요한가
HTTP는 본질적으로 at-least-once입니다. 네트워크가 타임아웃되면 클라이언트는 "도착했는지 아니면 도착하지 않았는지"를 판단할 수 없습니다. 사람이 조작하는 앱에서도 중복 제출(Double submit) 문제는 있었지만, 피해는 "버튼을 한 번 더 누른 1회분"으로 끝났습니다.
호출자가 자동화된 클라이언트(Job Scheduler, 재시도 메커니즘이 포함된 HTTP 클라이언트 SDK, 그리고 AI 에이전트)가 되면 이 전제가 무너집니다. AI 에이전트도 이러한 "자동화된 호출자"의 일종이며, 게다가 후술할 비결정성(Non-determinism) 때문에 다루기 어려운 부류에 속합니다. 대표적인 사고 패턴을 나열합니다.
- 결제 API를 배치 작업(Batch job)이 호출한 직후, 툴 실행 타임아웃(많은 프레임워크가 60초 전후로 끊습니다)에 도달하여 상위의 재시도 메커니즘이 자동 재전송한 결과, 동일한 고객에게 3번 과금되는 종류의 사고가 실제로 발생하고 있습니다. 사람이라면 "어라, 방금 눌렀던가" 하고 멈추겠지만, 자동화된 호출자는 멈추지 않습니다.
- Claude를 포함한 주요 모델은 사양으로서 병렬 툴 호출(Parallel tool call)을 지원합니다 (Anthropic docs: parallel tool use). 동일한 의도가 모델 측에서 두 개로 분할되어 발행되면, 서버 측은 양쪽 모두를 올바르게 처리해 버려 알림 메일이 2통 도착하는 종류의 현상이 발생합니다. 분할한 것은 모델이며, API 서버에는 잘못이 없습니다.
에이전트에게는 인간 클라이언트에게는 없는 성질이 있습니다.
- 재시도(Retry)가 빈번하게 발생합니다. 툴 에러, 컨텍스트 전환, 하네스(Harness)의 타임아웃이 겹치면 동일한 의도로 여러 번 호출됩니다. LangChain의 재시도 메커니즘이나 많은 Agents SDK가 "실패 시 재실행"을 기본값으로 하고 있습니다.
- 비결정성(Non-determinism)이 있습니다. 동일한 의도로부터 생성되는 요청 바디(Request body)가 미묘하게 흔들립니다. JSON의 키 순서나 생략 가능한 필드의 유무가 바뀌는 것만으로도 단순한 캐시 키(Cache key)는 효과가 없습니다.
- 보상(Compensation)이 작동하지 않는 경우가 있습니다. 환불 API나 취소 API가 존재하더라도, 에이전트가 그것을 확실히 호출 완료할 수 있다는 보장이 없습니다.
- 과거의 성공을 잊습니다. 세션이 끊긴 다음 턴에서 "다시 해보자"라고 판단할 수 있습니다.
이러한 거동을 "나쁜 에이전트"라고 치부하며 버려도 사고는 사라지지 않습니다. 서버 측에서 받아낼 메커니즘이 필요합니다.
Idempotency-Key의 메커니즘
현재 IETF에서 표준화가 진행 중인 드래프트는 draft-ietf-httpapi-idempotency-key-header입니다. 사양의 핵심은 심플합니다.
- 클라이언트가 고유한 값을 생성하여
Idempotency-Key헤더로 보냅니다 (드래프트 본문은 키의 고유성만을 요구합니다. UUID v4는 Stripe 등의 관행으로서 널리 사용되는 형식입니다). - 서버는 동일한 키로 재전송된 요청에 대해, 최초에 반환했던 상태 코드와 바디를 그대로 반환합니다.
- 동일한 키로 다른 페이로드(Payload)가 들어온 경우에는 에러(422 등)로 거부합니다.
- 키와 응답은 TTL(Time To Live)과 함께 유지합니다.
Stripe가 이 설계를 공개한 블로그 글을 작성한 것이 2017년이며, 그때부터 PayPal, Square, Shopify, GitHub, 일부 AWS API까지 널리 채택이 확산되었습니다. SDK 레벨에서 자동으로 키를 생성해 주는 것도 늘어나고 있습니다.
서버 측의 상태 전이(State Transition)는 실제로 구현해 보면 그림에서 보는 것만큼 단순하지 않습니다. 특히 "동일한 키가 동시에 in-flight(처리 중) 상태로 도착한" 케이스가 난관입니다. 단순히 코드를 작성하면, 락(Lock)을 획득하지 않은 채 양쪽 모두 처리를 실행해 버려 키의 존재 의의가 사라지게 됩니다.
서버 측 구현 포인트
스토리지 선정
저장해야 할 항목은 키(Key), 요청의 fingerprint, 응답(상태 코드와 바디), 상태(처리 중/완료), TTL입니다. 읽기와 쓰기가 동시에 발생하기 때문에 Redis나 DynamoDB와 같은 저지연 KVS(Key-Value Store)가 제1 후보가 됩니다. RDB로도 만들 수 있지만, TTL 관리와 행 잠금(Row Lock) 비용이 읽기 지연에 영향을 미칩니다.
스코프(Scope) 설계
키의 유일성은 "글로벌"하게 설정하지 않고, 최소한 (API 키 또는 테넌트 ID, 엔드포인트, 키 값)의 3가지 조합으로 스코프를 지정합니다. 이는 다른 테넌트의 키와 충돌하는 사고를 방지하고, 만일 키가 유출되더라도 영향 범위를 한정하기 위함입니다.
fingerprint 검증
동일한 키인데 바디(Body)가 다른 요청이 온다면 그것은 사고입니다. fingerprint를 저장해 두었다가, 재전송 시 일치하지 않으면 422 오류로 명시적으로 거부합니다. 이를 묵인하고 처리해 버리면, 나중에 "왜인지 응답이 예전 것이다"라는 추적하기 어려운 버그로 이어집니다.
락(Lock) 입도(Granularity)
동일한 키의 동시 도착에 대해서는 키 단위의 락이 필요합니다. Redis라면 SET key value NX PX ttl 패턴이 자주 사용됩니다. 처리 완료 후 락을 해제하면서 응답을 저장하는 순서는, 중간에 장애가 발생하더라도 "미완료 상태"가 남도록 해야 합니다. 미완료 상태로 장시간 경과한 키는 TTL로 자동 실효시키거나, 명시적으로 409 Conflict를 반환하여 재시도를 유도합니다.
TTL 설정
너무 짧으면 늦게 도착한 재전송을 새로운 요청으로 받아들여 이중 실행될 수 있습니다. 너무 길면 스토리지가 비대해집니다. Stripe는 24시간을 채택하고 있습니다. AI 에이전트를 상대한다면, 워크플로우 전체의 수명(도구 실행 타임아웃 + 재시도 횟수 × 백오프의 합계)보다 여유 있게 설정합니다. 기준은 24시간에서 7일 사이입니다.
// 키 핸들링 예시. 락 획득 → 재체크 → fingerprint 검증 → 처리 → 저장 순서
// fingerprint는 JSON 키 순서의 변동을 흡수하기 위해 정규화한 후 해싱한다
func handleIdempotent(ctx context.Context, key string, body []byte, exec func() Response) (Response, error) {
...
AI 에이전트(클라이언트) 측 구현 포인트
서버 측만 잘 갖춰 놓아도, 클라이언트가 매번 다른 키를 생성해 버린다면 의미가 없습니다. 에이전트 측 설계는 서버 이상으로 주의가 필요합니다.
키 생성은 에이전트에게 맡기지 말 것
LLM에게 "유니크한 키를 생성해 줘"라고 부탁하면, 순순히 UUID를 만들어 주기는 하지만 재시도 시에 동일한 키를 재현해 줄 것이라는 보장은 없습니다. "첫 번째에 생성한 UUID를 기억하고 두 번째에 재전송하는 것"은 하네스(Harness, tool wrapper)의 역할입니다. 이 부분을 모델에게 맡기면 재시도 시 키가 바뀌어 키의 의미가 사라집니다.
권장하는 방법은 하네스 측에서 결정론적(Deterministic)으로 키를 도출하는 것입니다.
// tool wrapper에서의 결정론적 키 생성. 표준 UUID v5 (namespace + name)를 사용
import { v5 as uuidv5 } from "uuid";
const AGENT_NAMESPACE = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; // 앱 고유의 namespace를 하나 결정
...
이렇게 하면 동일한 세션에서 동일한 도구를 동일한 입력으로 호출하는 한 키가 재현됩니다. 에이전트가 "다시 해보자"라고 판단하더라도, 서버는 첫 번째의 응답을 반환해 줍니다.
request_id나 trace ID로는 대체할 수 없음
"우리 API는 모든 요청에 유니크한 request_id를 부여하고 있다. 그것으로 중복 판정을 하면 Idempotency-Key는 필요 없는 것 아닌가"라는 반론을 자주 듣습니다. 결론부터 말씀드리면 대체할 수 없습니다. 역할이 다릅니다.
request_id나 OpenTelemetry의 trace_id
는 전송 계층(transport layer)에서 "1회의 HTTP 요청을 고유하게 식별하기" 위한 ID입니다. 재시도(retry)하면 다른 request_id가 부여됩니다. 이는 로그 추적 및 분산 트레이싱(distributed tracing)의 전제 조건이며, 동일한 ID를 재사용하면 트레이싱 측이 망가집니다.
반면 Idempotency-Key가 식별하는 것은 전송이 아니라 "의도(intent)"입니다. "같은 주문을 다시 한번 확정하고 싶다"라는 의도의 한 단위에 대해 하나의 키가 할당되며, 그 의도가 5번 재시도되더라도 계속해서 같은 키를 사용합니다. 유니크(unique)한지 여부가 아니라, "같은 의도라면 같고, 다른 의도라면 다르다"라는 사상(mapping)이라는 점이 본질입니다.
두 가지를 대비하여 표로 나타내면 이해하기 쉽습니다.
| 관점 | request_id / trace_id | Idempotency-Key |
|---|---|---|
| 식별 대상 | 1회의 HTTP 요청 (전송 계층) | 1개의 의도 (애플리케이션 계층) |
| ... |
만약 request_id를 재시도 간에 재사용하는 설계로 만든다면, 그것은 이름만 request_id일 뿐 실질적으로는 Idempotency-Key입니다. 그 경우에는 트레이싱 측에서 별도의 스팬 ID(span ID) 등이 필요하게 됩니다. 두 가지를 하나의 ID로 겸하는 설계는, 한쪽의 요구사항을 만족하면 다른 한쪽이 망가집니다. 솔직하게 별도의 이름과 별도의 생성 규칙을 가진 두 개의 ID를 갖는 것이 결국 더 심플합니다.
의도적인 재시도와 신규 요청의 구별
키를 결정적으로 만들면 역의 문제가 발생합니다. 사용자가 정말로 "한 건 더, 같은 내용으로 주문해 줘"라고 말했을 경우, 키가 같다면 두 번째 건은 주문되지 않습니다. 구별을 위해서는 키 도출(derivation)의 입력값에 의도의 식별자를 섞습니다. 사용자의 발화 턴(turn) 번호, 요청의 시퀀스(sequence) 번호, 혹은 사용자가 명시한 request_id를 포함합니다.
병렬 tool call의 처리
Claude를 포함한 주요 모델은 동일한 의도를 병렬 tool call로 분할하는 경우가 있습니다. 하네스(harness) 측에서 "동일한 키가 동시에 in-flight 상태"임을 감지하면, 한쪽을 기다리게 하거나 명시적으로 한쪽을 버리는 메커니즘을 넣습니다. 서버 측에서 409를 반환해 준다면, 클라이언트는 재시도를 통해 기다리면 됩니다.
실패 관측을 묵인하지 않기
422나 409가 반환되었을 때, 에이전트에게 "다른 입력으로 시도해 봐"라고 말하면 무한 루프에 빠져 계속해서 다른 키를 생성하게 됩니다. 하네스 측에서 "이것은 재시도 억제 시그널이며, 입력을 바꿔야 하는 에러가 아니다"라고 분류하여, 모델의 재시도 판단에서 제외합니다.
함정과 안티 패턴(Anti-pattern)
현장에서 흔히 보이는 전형적인 실패 사례를 몇 가지 꼽아보겠습니다.
- 키 생성을 모델에 맡긴 결과, 재시도 시 다른 UUID가 생성되어 이중 결제가 발생했다
- fingerprint 검증을 생략하여, 같은 키로 다른 바디(body)를 재전송하는 것을 그대로 통과시켰다. 몇 달 후 "응답이 오래되었다"라는 장애로 발견되었다
- TTL을 1시간으로 설정하여, 심야 배치 작업에서 지연된 재전송을 신규 요청으로 취급했고, 다음 날 아침의 주문이 두 배가 되었다
- 스토리지로 RDB를 선택하여, in-flight 락(lock)이 행 잠금(row lock)과 충돌해 API 전체의 레이턴시(latency)가 급증했다
- 스코프(scope)를 글로벌(키만 존재)로 설정하여, 다른 테넌트(tenant)의 구현 버그로 키가 충돌했고, 무관한 고객에게 타인의 응답이 반환되었다
모두 리뷰 과정에서 눈치챌 수 있는 이야기들이지만, 재시도 경로와 스코프 설계는 운영 환경에서 문제가 드러나기 전까지는 알아차리기 어려운 영역입니다.
Idempotency-Key를 사용하지 않는 대안과 그 한계
"Idempotency-Key 헤더를 새로 도입하기보다, 이미 있는 메커니즘으로 해결하고 싶다"라는 판단은 당연히 나올 수 있습니다. 실무에서 자주 사용되는 대안을 효과가 있는 조건과 함께 정리합니다. 대부분은 "다른 이름으로 같은 문제를 풀고 있는 것"에 불과하지만, 조건이 맞는 상황에서는 오히려 이쪽이 더 솔직한 선택이 됩니다.
업무상의 자연 키(Natural Key)에 UNIQUE 제약 조건 부여
orders 테이블의 order_number, invoices의 invoice_id와 같이, 업무 측면에서 유일성이 정의될 수 있는 컬럼에 DB의 UNIQUE 제약 조건을 겁니다. 이중 INSERT는 RDB가 차단해 줍니다.
- 효과적인 케이스: 주문, 청구서, 예약 번호와 같이 업무 측면에서 "중복되어서는 안 되는 단위"가 명확한 조작
- 효과가 없는 케이스: 알림 전송, 외부 API 호출, 감사 로그(audit log) 기록과 같이 "같은 내용이 반복해서 발생해도 되는" 조작. 예를 들어 "사용자 A에 대한 결제 완료 알림"은 오늘도 내일도 반복해서 발생할 수 있으므로,
(user_id, message_type)
과 같은 열에 UNIQUE 제약 조건을 걸 수는 없습니다. 네트워크 재전송에 의한 우발적인 중복과 업무상 정당한 반복을 테이블 정의만으로 구분할 수단이 없기 때문입니다. - 본질적인 한계: 의도의 식별자를 업무 데이터 내에서 찾을 수 있는 경우에만 사용할 수 있습니다. "이번의 이 한 번"을 가리키는 정보가 업무 데이터에 나타나지 않는다면, DB 제약 조건으로는 판정할 수 없습니다.
클라이언트 생성 리소스 ID + PUT
POST /orders가 아니라 PUT /orders/{clientGeneratedId}를 사용합니다. HTTP 사양상 PUT은 멱등성 (Idempotency)을 가지므로, 동일한 ID로 몇 번을 PUT 하더라도 최종 상태는 동일합니다.
- 효과적인 케이스: 리소스 생성 계열. RESTful 원칙상 이것이 이상적입니다.
- 한계: 클라이언트가 "동일한 의도에는 동일한 ID를, 다른 의도에는 다른 ID를" 할당해야 합니다. 이는 결국 Idempotency-Key와 동일한 규율이며, 이름만 바뀐 것뿐입니다.
- 부작용: URI를 설계 시점에 결정해야 하므로, 기존의
POST /orders기반 API에는 사후 적용하기 어렵습니다.
2페이즈 방식 (reserve → confirm)
먼저 POST /payments/reservations를 통해 reservation ID를 반환받고, 이어서 POST /payments/{reservationId}/confirm으로 확정합니다. confirm은 reservation ID에 종속되므로 자연스럽게 멱등성을 갖게 됩니다.
- 효과적인 케이스: 결제 게이트웨이, 재고 예약.
- 비용: 왕복(Round-trip)이 1회 증가합니다. 방치된 reservation에 대한 GC (Garbage Collection)도 필요합니다.
- 함정: 첫 번째 reservation 생성 자체가 리트라이(Retry)된다면, reservation이 이중으로 생성됩니다. 결국 거기에도 Idempotency-Key가 필요합니다.
Optimistic Concurrency (ETag / If-Match / version)
서버가 반환한 ETag 또는 버전 번호를 다음 요청 시 If-Match 헤더로 보내게 합니다. 버전이 일치하지 않으면 412 Precondition Failed를 반환합니다.
- 효과적인 케이스: 수정 계열 (PATCH/PUT). "타인이 먼저 수정했다면 실패시키고 싶을 때" 사용하는 용도.
- 효과 없는 케이스: 신규 생성 계열. 생성 전에는 ETag가 존재하지 않으므로, 이중 POST를 막을 수 없습니다.
큐 계층의 중복 제거 (SQS FIFO / Kafka exactly-once)
API 서버 앞단에 큐를 배치하고, 큐의 중복 제거 (dedup) 기능에 맡깁니다. SQS FIFO라면 5분간의 중복 제거 윈도우가 있으며, Kafka라면 프로듀서 (producer)의 멱등성 (idempotence) 설정이 있습니다.
- 효과적인 케이스: 비동기 처리가 허용되는 도메인.
- 효과 없는 케이스: "결제 성공했습니다"를 즉시 반환해야 하는 동기식 응답 용도로는 사용할 수 없습니다.
- 본질: 큐의 중복 제거 ID가 실질적으로 Idempotency-Key입니다. 책임이 API 계층에서 미들웨어 계층으로 옮겨졌을 뿐입니다.
원타임 토큰 (Form CSRF 방식)
서버가 사전에 POST /orders/tokens를 통해 토큰을 발행하고, 실제 POST 시점에 이를 보내게 합니다. 사용된 토큰은 무효화합니다.
- 효과적인 케이스: 인간 사용자의 브라우저 조작.
- AI 에이전트에게는 부적합: 에이전트는 사전 토큰 취득 단계도 리트라이하므로, 토큰 취득 자체가 이중 실행되면 근본적인 해결책이 되지 않습니다.
클라이언트 측 single-flight
에이전트의 하네스 (harness) 측에서 "동일한 의도의 in-flight 요청은 단 1개만"이라는 규칙을 엄격히 준수합니다. Go 언어의 singleflight.Group 패턴과 같습니다.
- 효과적인 케이스: 단일 프로세스 · 단일 에이전트 내부.
- 효과 없는 케이스: 프로세스 재시작을 넘나드는 리트라이, 복수 에이전트의 병렬 실행, 하네스 외부에서 발생하는 타임아웃 기인 재전송. 분산 환경에서는 성립하지 않습니다.
대안 및 본 기사의 위치
다음의 성질을 모두 만족하는 대안은 Idempotency-Key뿐입니다.
- 업무 데이터에 자연 키 (Natural Key)가 없어도 사용할 수 있음
- 동기식 응답을 반환할 수 있음
- 신규 생성에 효과적임
- 클라이언트가 분산되어 있어도 기능함
- HTTP의 표준적인 형태에 부합함
그렇긴 하지만 「자연 키(Natural Key)의 UNIQUE 제약」과 「클라이언트 생성 ID + PUT」은 조건이 맞는 상황에서는 오히려 Idempotency-Key보다 더 직관적이고 강력합니다. 도메인의 일부(주문 번호 등)는 UNIQUE 제약, 그렇지 않은 부분(알림 전송, 결제 intent, 범용적인 RPC)은 Idempotency-Key를 사용하는 방식의 병용이 현실적인 해답이 되는 경우가 많습니다.
요약
조건과 선택해야 할 설계를 정리해 둡니다.
| 조건 | 선택해야 할 설계 |
|---|---|
| GET이나 HEAD 등 본래 멱등(Idempotent)한 엔드포인트 | Idempotency-Key 불필요 |
| ... |
지금 바로 할 수 있는 일
규모에 관한 이야기를 하기 전에, 이 글을 다 읽은 직후부터 취할 수 있는 구체적인 액션을 3가지로 압축합니다.
- 리포지토리 내의 부작용(Side Effect)이 있는 POST/PATCH 엔드포인트를 전수 조사합니다.
grep -r "POST" routes/명령어로 기계적으로 찾아내어, 결제·알림·외부 API 호출과 관련된 것에 표시를 해둡니다. - 각 엔드포인트에 대해
Idempotency-Key헤더 수용 여부와 fingerprint 검증 유무를 체크합니다. 없다면 Issue를 생성합니다. - AI 에이전트로부터 호출될 가능성이 있는 엔드포인트에 대해, 하네스(Harness) 측의 키 생성 방식(결정적(Deterministic) 방식인지, 매번 생성하는 방식인지)을 확인합니다. 매번 생성하는 방식이라면 재시도(Retry) 시 다른 키가 전송되므로, 결정적 방식으로 전환합니다.
더욱 대규모로 스케일링한다면
규모가 변하면 설계의 무게 중심도 이동합니다. 요청 수의 기준으로 작성합니다.
쓰기 부하가 높아지면 키의 KVS(Key-Value Store)가 병목 지점이 됩니다. 키 1개당 평균 응답 크기가 2KB이고 하루 1,000만 건의 요청이 있다면, 24시간 TTL 기준으로 약 20GB의 메모리가 필요합니다. 단일 Redis 노드로도 수용 가능하지만, 월간 수십억 건 규모에 달하면 테넌트 ID 단위의 샤딩(Sharding)이 필요해집니다. 동시에, fingerprint 저장을 「응답 전체」에서 「응답의 해시 + S3 등으로의 포인터」로 전환하면 KVS의 메모리 소비를 10분의 1로 줄일 수 있습니다.
키의 수명이 길어져 수십억 건을 넘어가게 되면, TTL에 의존한 삭제가 Redis의 메모리 관리를 압박합니다. Redis의 액티브 만료(Active Expire)는 CPU를 소비하며, 이는 p-퍼센타일(p-percentile) 레이턴시로 나타납니다. DynamoDB의 TTL 기능(백그라운드 삭제로 클라이언트 측 레이턴시에 영향 제로)으로 전환하거나, 일 단위 파티션으로 오래된 키를 일괄 DROP하는 운영 방식으로 전환합니다.
에이전트 측의 워크플로우가 장시간화되는 경우, 단일 Idempotency-Key만으로는 부족하며, 워크플로우 단위의 ID와 그 안의 각 스텝(Step) 키로 구성된 2층 구조로 만듭니다. 상위는 Saga 패턴을 통한 실행 트레이스(Trace), 하위는 각 API 호출의 멱등성 키라는 역할 분담을 하면, 실패 시 재개 위치 파악과 중복 방지를 모두 처리할 수 있습니다.
참고가 될 만한 자료를 몇 가지 나열해 둡니다. 설계 시 꼭 참조하시기 바랍니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기