이 페이지에서 다루는 것
API Idempotency
한 번에 끝까지 읽으며 맥락을 쌓을 수 있도록 구성했습니다.
AI가 만든 결제·주문·알림 API에서 중복 요청, 재시도 폭주, 부분 성공을 막는 실전 설계 루프
학습 유형
주제 심층 학습
핵심 주제
API Idempotency
키워드
API 멱등성 · idempotency key · 재시도 폭주 · 중복 결제 방지 · 웹훅 재처리 · AI API 개발
이 페이지에서 다루는 것
API Idempotency
한 번에 끝까지 읽으며 맥락을 쌓을 수 있도록 구성했습니다.
예상 학습 시간
16분
본문과 보조 자료(이미지·영상)를 포함한 대략적인 소요입니다.
학습 팁
섹션 순서대로 읽고, 필요한 부분만 다시 찾아보기
표·이미지·영상은 본문 흐름을 돕는 보조 설명입니다.
AI가 API를 빠르게 만들면 처음에는 버튼이 눌리고 응답이 돌아오는지만 보게 됩니다. 하지만 실제 서비스에서 더 위험한 순간은 버튼이 한 번 눌렸는데 네트워크가 끊기고, 사용자가 새로고침하고, 브라우저가 같은 요청을 다시 보내고, 백그라운드 워커가 실패한 웹훅을 재처리하는 순간입니다. 이때 멱등성 없이 만든 API는 중복 주문, 중복 결제, 중복 알림, 재고 차감 오류를 만들 수 있습니다.
멱등성은 “같은 의도의 요청이 여러 번 들어와도 결과가 한 번 처리된 것처럼 보장하는 성질”입니다. 초보자에게는 어려운 단어처럼 보이지만, 실무에서는 매우 구체적입니다. 클라이언트가 요청마다 고유한 idempotency key를 보내고, 서버는 그 키와 사용자, 작업 종류, 요청 본문 해시, 처리 상태를 저장합니다. 같은 키가 다시 오면 새 작업을 만들지 않고 이전 결과를 돌려주거나, 처리 중임을 알려주거나, 본문이 달라졌다면 거부합니다.
VIBE 코딩에서 이 주제를 따로 다뤄야 하는 이유는 AI가 “재시도하면 됩니다”라는 코드를 너무 쉽게 만들기 때문입니다. 재시도 자체는 나쁘지 않습니다. 문제는 재시도 정책, 멱등성 저장소, 상태 전이, 로그 상관관계, 승인 기준이 함께 설계되지 않은 재시도입니다. 이 글은 AI에게 API를 맡길 때 중복 실행을 막는 작업 루프를 만드는 방법입니다.
결제, 주문, 포인트 지급, 메일 발송, 알림 전송, 외부 API 호출, 웹훅 수신처럼 “두 번 실행되면 손해가 나는 작업”에는 멱등성 가드레일을 기본값으로 넣어야 합니다. 단순히 프론트 버튼을 비활성화하는 것으로는 부족합니다. 사용자는 같은 브라우저만 쓰지 않고, 네트워크는 항상 안정적이지 않으며, 외부 서비스는 같은 웹훅을 여러 번 보낼 수 있습니다.
좋은 API 루프는 네 가지를 동시에 갖습니다.
| 영역 | 좋은 기준 | 나쁜 신호 |
|---|---|---|
| 요청 식별 | 사용자·작업·idempotency key·본문 해시를 함께 기록 | 단순히 timestamp나 random 값만 믿음 |
| 상태 전이 | pending, succeeded, failed, expired를 명확히 구분 | 실패와 처리 중을 모두 500으로 뭉갬 |
| 재시도 정책 | 지수 백오프, 최대 횟수, 재시도 가능한 오류 목록 | 무한 재시도 또는 즉시 반복 호출 |
| 검증 | 같은 키 2회, 본문 변경, 동시 요청, 외부 실패를 테스트 | 성공 케이스 한 번만 수동 클릭 |
AI에게 “중복 요청 막아줘”라고만 시키면 대개 프론트 버튼 disable이나 간단한 중복 체크만 나옵니다. 대신 “서버 기준 멱등성 키 저장, 요청 본문 해시 검증, 동시 요청 잠금, 이전 응답 재사용, 관측 가능한 로그까지 포함하라”고 지시해야 합니다.
API 중복 실행은 코드가 틀렸을 때만 생기지 않습니다. 정상적인 네트워크에서도 생깁니다. 모바일 사용자가 지하철에서 결제 버튼을 누르고 연결이 끊길 수 있습니다. 브라우저가 타임아웃 후 같은 요청을 다시 보낼 수 있습니다. 결제 대행사는 성공 웹훅을 보냈지만 내 서버 응답을 받지 못해 같은 이벤트를 다시 보낼 수 있습니다. 백그라운드 큐는 작업자가 죽으면 같은 job을 재전달할 수 있습니다.
서비스 입장에서 문제는 “요청이 두 번 들어왔다”가 아니라 “같은 의도의 작업이 두 번 커밋됐다”입니다. 사용자는 한 번 결제했다고 생각하는데 주문이 두 개 생기면 고객센터 비용이 생깁니다. 알림이 두 번 가면 신뢰가 떨어집니다. 포인트가 두 번 지급되면 회계 정합성이 깨집니다. 외부 API 비용이 두 번 청구되면 작은 버그가 바로 비용 사고가 됩니다.
AI 코딩 루프에서는 이 위험이 더 커집니다. AI는 happy path를 빨리 완성하고, “실패하면 다시 시도”라는 코드를 그럴듯하게 붙입니다. 그런데 재시도에는 반드시 중단 조건이 있어야 하고, 중복 실행에는 반드시 서버 기준 방어선이 있어야 합니다. 이 둘이 없으면 자동화가 빠를수록 장애도 빠르게 반복됩니다.
가상의 예로 AI가 “강의 구매 API”를 만들었다고 합시다. 사용자가 구매 버튼을 누르면 서버는 결제 승인 API를 호출하고, 성공하면 주문 row를 만들고, 강의 접근 권한을 열고, 구매 완료 메일을 보냅니다. 첫 시연에서는 잘 됩니다. 하지만 운영에서는 다음 일이 생깁니다.
사용자는 결제 버튼을 눌렀고 외부 결제는 성공했습니다. 그런데 브라우저에는 응답이 늦게 와서 로딩이 계속됩니다. 사용자가 새로고침 후 다시 누르면 서버는 새 주문을 만들 수 있습니다. 이때 프론트 버튼 disable은 이미 사라졌습니다. 서버가 idempotency key를 기준으로 이전 성공 결과를 찾아 돌려줘야 합니다.
결제사는 내 서버가 200 응답을 주지 못했다고 판단하고 같은 결제 성공 웹훅을 다시 보냅니다. 웹훅 핸들러가 이벤트 id 또는 결제 id 기준 멱등성을 갖지 않으면 접근 권한 부여, 메일 발송, 내부 알림이 반복됩니다. 특히 “메일 발송은 부작용이 작다”고 방치하면 운영자는 중복 문의를 받게 됩니다.
주문 row는 만들어졌지만 메일 발송이 실패할 수 있습니다. 이때 전체 요청을 실패로 돌리고 사용자가 다시 누르게 만들면 주문 자체가 중복될 수 있습니다. 주문 생성과 후속 알림은 같은 트랜잭션처럼 보이지만 실제로는 다른 신뢰 경계에 있습니다. 핵심 작업과 부가 작업을 구분해야 합니다.
사용자가 더블클릭하거나 브라우저 탭 두 개에서 같은 작업을 실행하면 거의 동시에 같은 idempotency key가 들어올 수 있습니다. 단순 조회 후 insert 방식은 race condition을 만들 수 있습니다. 저장소의 unique constraint, upsert, transaction, row-level lock 또는 원자적 상태 전환이 필요합니다.
모든 API에 같은 수준의 멱등성을 붙일 필요는 없습니다. 읽기 API는 캐시와 rate limit이 더 중요할 수 있습니다. 하지만 결제, 주문, 예약, 포인트, 권한 부여, 이메일 발송, 외부 유료 API 호출, 파일 변환 job 생성, 웹훅 수신은 별도 목록으로 표시해야 합니다.
AI에게는 이렇게 분류를 먼저 요구합니다. “현재 변경 범위의 API를 읽기, 단순 쓰기, 비용·권한·외부 부작용 쓰기로 나누고, 비용·권한·외부 부작용 쓰기에는 멱등성 설계를 제안하라.” 이 한 문장만 넣어도 AI가 단순 CRUD 관점에서 벗어납니다.
클라이언트 생성 키인지 서버 발급 키인지 정해야 합니다. 결제나 주문처럼 사용자가 버튼을 누르는 흐름에서는 서버가 checkout session을 만들 때 키를 발급하고, 클라이언트는 그 키를 다음 요청에 붙이는 방식이 안전합니다. 외부 웹훅은 외부 이벤트 id를 키로 삼되, 공급자 이름과 이벤트 종류를 함께 묶어야 합니다.
키만 저장하면 부족합니다. 같은 키로 완전히 다른 본문이 오면 위험합니다. 서버는 요청 본문에서 안정적인 필드만 정렬해 hash를 만들고 함께 저장해야 합니다. 같은 키와 같은 hash면 이전 결과를 재사용하고, 같은 키인데 hash가 다르면 409 conflict로 거부합니다.
최소 필드는 key, userId 또는 actorId, operation, requestHash, status, responseSummary, errorCode, createdAt, updatedAt, expiresAt입니다. 상태는 pending, succeeded, failed, expired 정도로 시작하면 충분합니다. 중요한 점은 failed를 무조건 재시도 가능으로 보지 않는 것입니다. 카드 한도 초과 같은 비즈니스 실패는 같은 키로 다시 실행해도 성공하지 않을 수 있습니다.
동시 요청을 막으려면 key와 operation에 unique constraint를 둡니다. 첫 요청이 pending을 만들고 작업을 시작합니다. 두 번째 요청이 들어오면 새 작업을 만들지 않고 pending 상태를 확인해 “처리 중” 응답을 주거나 짧게 polling하도록 안내합니다. 성공 후에는 responseSummary를 저장해 같은 요청에 동일한 결과를 돌려줍니다.
외부 결제 승인, 이메일 발송, 알림 전송은 실패 가능성이 다릅니다. 주문 생성이 성공했는데 이메일만 실패했다면 주문을 다시 만들면 안 됩니다. 핵심 커밋과 후속 부작용을 구분하고, 후속 작업은 outbox 또는 queue에 넣어 재처리합니다. 이때 outbox event에도 고유 키를 붙여 같은 메일이 반복되지 않도록 합니다.
초보자가 이해하기 쉽게 말하면 “주문이 만들어졌는가”와 “메일이 갔는가”는 다른 체크박스입니다. 하나가 실패했다고 다른 하나를 다시 실행하면 안 됩니다. AI에게도 이 분리를 명시해야 합니다.
재시도는 감정이 아니라 숫자입니다. 예를 들어 네트워크 타임아웃, 429, 502, 503, 504는 최대 3회 지수 백오프로 재시도합니다. 400, 401, 403, 404, 409는 기본적으로 재시도하지 않습니다. 결제 승인 후 확인 API처럼 중요도가 높은 작업은 사람이 확인할 수 있는 보류 상태를 둡니다.
지수 백오프도 상한이 필요합니다. 1초, 3초, 10초처럼 늘리되 총 대기 시간과 최대 횟수를 정합니다. 큐 작업이라면 dead letter 상태와 재처리 버튼의 권한도 정합니다. “계속 재시도”는 운영 정책이 아니라 장애 증폭 장치입니다.
문제가 생겼을 때 “왜 두 번 처리됐는지”를 알아야 합니다. 로그에는 requestId, idempotencyKey의 안전한 일부, operation, actorId의 내부 식별자, previousStatus, nextStatus, retryAttempt, externalProvider, externalEventId, resultCode를 남깁니다. 키 전체나 사용자 개인정보를 그대로 남기지 말고, 앞뒤 몇 글자 또는 해시로 추적 가능한 수준만 기록합니다.
관측 가능성이 없으면 멱등성은 믿음이 됩니다. 운영자는 같은 키가 몇 번 들어왔는지, pending이 오래 남았는지, failed가 재시도되고 있는지, succeeded가 중복 응답으로 재사용됐는지 볼 수 있어야 합니다.
결제 승인 API는 클라이언트가 같은 checkoutId와 idempotency key를 보내도록 만듭니다. 서버는 먼저 멱등성 row를 만들고, 이미 succeeded면 기존 주문 번호를 반환합니다. pending이면 새 결제 승인을 호출하지 않고 처리 중 응답을 줍니다. requestHash가 다르면 409로 멈춥니다.
승인 API 호출이 성공했지만 주문 저장에서 실패했다면 가장 위험합니다. 이때는 무작정 다시 승인하지 말고 provider transaction id로 조회해 이미 승인된 결제를 복구하는 보상 루프가 필요합니다. AI에게 “외부 결제 성공 후 내부 저장 실패 복구 경로를 설명하라”고 요구해야 합니다.
웹훅은 보통 공급자가 같은 이벤트를 여러 번 보낼 수 있다고 가정해야 합니다. provider, eventId, eventType 조합을 unique key로 저장합니다. 이미 처리된 이벤트면 200을 반환하되 내부 작업을 반복하지 않습니다. 아직 처리 중이면 202 또는 200으로 받아 두고 중복 실행을 막습니다.
웹훅 본문 검증도 먼저 해야 합니다. 서명 검증이 실패한 요청은 멱등성 저장소에 성공 이벤트처럼 남기지 않습니다. 서명 검증, 이벤트 중복 체크, 상태 전이, 후속 작업 발행 순서가 중요합니다.
이메일은 “두 번 가도 큰일은 아니다”라고 생각하기 쉽지만, 인증 메일, 결제 완료 메일, 초대 메일은 신뢰에 영향을 줍니다. messageDedupKey를 만들어 recipient, template, businessObjectId를 묶습니다. 같은 주문에 같은 템플릿은 한 번만 보내고, 실패하면 재시도 횟수를 기록합니다.
알림 발송은 사용자 경험과 비용 모두에 영향을 줍니다. 푸시, 이메일, 메신저를 동시에 보낼 때 각 채널마다 멱등성 키를 따로 두되, 상위 notificationIntentId로 묶어야 전체 흐름을 추적할 수 있습니다.
AI 에이전트에게 “이 리포트를 만들어라” 같은 장기 작업을 만드는 API도 멱등성이 필요합니다. 사용자가 버튼을 여러 번 누르면 같은 비싼 작업이 여러 개 생성될 수 있습니다. promptHash, inputDatasetId, userId, operation을 묶어 같은 작업 의도인지 판단하고, 이미 running이면 기존 jobId를 보여주는 방식이 좋습니다.
이 경우 비용 예산과도 연결됩니다. 같은 요청이 세 번 실행되면 토큰 비용과 외부 API 비용이 세 배가 됩니다. 멱등성은 안정성뿐 아니라 비용 통제 장치입니다.
다음 지시문은 그대로 복사하기 위한 템플릿이 아니라, AI에게 어떤 사고 순서를 요구해야 하는지 보여주는 예시입니다.
이 API는 중복 실행되면 비용 또는 권한 문제가 생기는 쓰기 작업이다. 먼저 위험한 부작용을 목록화하고, 서버 기준 idempotency key 계약을 설계하라. 같은 key와 같은 request hash는 이전 결과를 반환하고, 같은 key와 다른 request hash는 409로 거부하라. 동시 요청 race condition을 막기 위해 unique constraint 또는 원자적 upsert를 사용하라. 외부 API 성공 후 내부 저장 실패, 내부 저장 성공 후 후속 알림 실패를 각각 복구 경로로 나누어 설명하라. 재시도 가능한 오류와 불가능한 오류를 HTTP status와 provider error code 기준으로 분리하고, 최대 재시도 횟수와 백오프를 숫자로 제안하라. 테스트에는 같은 요청 2회, 같은 key 다른 본문, 동시 요청 2개, provider timeout, 웹훅 중복 수신, pending 오래 지속 케이스를 포함하라. 로그에는 requestId, 안전하게 축약한 idempotency key, operation, previousStatus, nextStatus, retryAttempt를 남기되 민감정보는 출력하지 말라.
작업을 맡길 때는 구현 파일만 요구하지 말고 테스트 계획도 함께 요구해야 합니다. 특히 동시 요청 테스트는 AI가 자주 빠뜨립니다. 동시에 두 요청을 보내 한 개의 row와 한 개의 외부 호출만 생기는지 확인해야 합니다.
첫 번째 실패 모드는 “클라이언트만 믿는 멱등성”입니다. 프론트가 버튼을 비활성화해도 네트워크 재시도, 탭 중복, 외부 웹훅, 모바일 앱 재전송은 서버로 들어옵니다. 서버 저장소가 기준이 되어야 합니다.
두 번째 실패 모드는 “키는 같지만 본문이 다른 요청”입니다. 사용자가 장바구니를 바꿨는데 같은 키가 재사용될 수 있습니다. requestHash 검증이 없으면 이전 결과를 잘못 돌려주거나 다른 주문을 막아 버립니다.
세 번째 실패 모드는 “pending이 영원히 남는 상태”입니다. 작업자가 죽었는데 pending row가 계속 남으면 사용자는 계속 처리 중 메시지만 봅니다. pending에는 만료 시간, 복구 job, 수동 확인 기준이 있어야 합니다.
네 번째 실패 모드는 “실패를 모두 재시도하는 정책”입니다. 인증 실패, 권한 부족, 본문 충돌, 비즈니스 거절은 재시도해도 해결되지 않습니다. 재시도는 일시 장애에만 적용해야 합니다.
다섯 번째 실패 모드는 “이전 응답을 그대로 저장하다가 개인정보를 노출하는 것”입니다. responseSummary에는 공개 가능한 주문 번호, 상태, 화면 메시지처럼 최소 정보만 저장합니다. 카드 정보, 토큰, 전체 외부 응답, 민감한 사용자 입력은 저장하지 않습니다.
여섯 번째 실패 모드는 “멱등성 저장소 자체를 정리하지 않는 것”입니다. 모든 key를 영원히 보관하면 저장소가 커지고 개인정보 보존 이슈가 생깁니다. 작업 종류별 TTL을 둡니다. 결제나 회계와 연결된 기록은 감사 정책에 맞게 더 오래 보관하되, 일반 알림 중복 방지 key는 짧게 가져갈 수 있습니다.
이 체크리스트는 구현 전에도 쓸 수 있습니다. AI가 제안한 설계를 검토할 때 각 항목에 답하지 못하면 아직 작업 지시가 부족한 것입니다.
승인 기준은 “테스트가 통과했다”보다 구체적이어야 합니다. 최소 승인 기준은 다음과 같습니다. 첫째, 중복 요청 테스트에서 외부 호출 mock count가 1이어야 합니다. 둘째, 같은 key 다른 본문 테스트가 409를 반환해야 합니다. 셋째, 동시 요청 테스트에서 성공 row가 하나만 생겨야 합니다. 넷째, 웹훅 중복 수신이 내부 이벤트를 한 번만 발행해야 합니다. 다섯째, 로그 샘플에 민감정보가 없어야 합니다.
중단 기준도 숫자로 정합니다. 예를 들어 결제 승인 API에서 provider timeout 후 내부 상태가 불명확하면 자동 재승인을 중단하고 보류 상태로 전환합니다. 같은 key의 pending 상태가 10분 이상 지속되면 복구 job 또는 운영자 확인 큐로 이동합니다. 5분 동안 같은 operation에서 409가 평소보다 3배 이상 늘면 클라이언트 키 생성 버그를 의심하고 배포를 멈춥니다. 재시도 큐의 dead letter 비율이 1%를 넘으면 자동 재처리를 중단하고 원인 분석을 먼저 합니다.
실무에서는 “계속 시도”보다 “여기서 멈춘다”가 더 중요합니다. AI 에이전트에게도 중단 기준을 명시해야 합니다. 자동화가 멈추는 지점을 정해야 사람이 안전하게 승인할 수 있습니다.
처음부터 완벽한 멱등성 플랫폼을 만들 필요는 없습니다. 가장 위험한 API 하나를 고르고, idempotency key 저장소와 중복 요청 테스트부터 붙이세요. 그 다음 웹훅, 알림, 큐 작업으로 확장하면 됩니다. 이미 운영 중인 서비스라면 최근 30일 로그에서 같은 사용자, 같은 작업, 짧은 시간 안에 반복된 요청을 찾아 위험 API 후보를 고를 수 있습니다.
VIBE 코딩의 목표는 AI가 코드를 많이 쓰게 하는 것이 아니라, AI가 만든 코드가 운영에서 반복 가능한 실패를 만들지 않게 하는 것입니다. 멱등성·재시도 가드레일은 그 목표를 가장 현실적으로 보여주는 주제입니다. 한 번 처리해야 할 작업은 한 번만 처리되고, 다시 들어온 요청은 안전하게 같은 결과를 보게 만드는 것. 이것이 API를 제품 수준으로 끌어올리는 기본 운영 루프입니다.
아니요. 프론트 키는 도움이 되지만 서버 저장소가 기준이어야 합니다. 서버가 키, 사용자, 작업 종류, 요청 본문 해시, 상태를 저장하고 재요청을 판정해야 네트워크 재시도나 웹훅 중복에도 안전합니다.
결제, 주문, 예약, 권한 부여, 포인트 지급, 외부 유료 API 호출, 이메일·푸시 발송, 웹훅 수신처럼 두 번 실행되면 비용이나 신뢰 문제가 생기는 API부터 적용하세요.
요청 본문 해시를 함께 저장하고, 같은 키지만 해시가 다르면 새 작업으로 처리하지 말고 409 conflict처럼 명확한 오류로 거부하는 것이 안전합니다.
그렇지 않습니다. 일시 장애에는 지수 백오프와 최대 횟수가 필요하지만, 인증 실패나 본문 충돌 같은 오류는 재시도해도 해결되지 않습니다. 무한 재시도는 장애와 비용을 키웁니다.
서버 기준 멱등성 저장소, request hash 검증, 동시 요청 테스트, 외부 성공 후 내부 실패 복구, 재시도 가능한 오류 분류, 민감정보 없는 로그를 모두 포함하라고 명시해야 합니다.
다음 학습
VIBE 코딩으로 결제 알림, 가입 완료 알림, 배포 이벤트, 폼 제출, 자동화 웹훅을 만들 때 가장 위험한 착각은 “요청이 한 번만 온다”는 믿음입니다. 실제 서비스에서는 네트워크 재시도, 외부 서비스의 중복 전송, 사용자의 새로고침, 작업 큐 재처리, 에이전트의 반복 실행 때문에 같은 의미의 요청이 여러 번 들어옵니다. 이때 중복을 막는 설계가 없으면 한 사람에게 쿠폰이 두 번 지급되고, 같은 알림이 세 번 발송되고, 결제 성공 이벤트 하나가 여러 개의 주문 처리로 번질 수 있습니다.
이 글의 목표는 웹훅을 빠르게 붙이는 법이 아니라, AI가 만든 웹훅을 실제 서비스에 넣어도 안전한 운영 루프로 만드는 법입니다. 초보자는 “같은 요청이 또 와도 결과가 한 번만 적용되게 만든다” 정도로 이해하면 됩니다. 전문가 관점에서는 요청 식별자, 원자…
AI가 만든 기능이 운영 환경에서 실패할 때 가장 위험한 대응은 “에러 메시지를 더 많이 찍어 보자”로 시작하는 것입니다. 로그가 많아지면 원인이 더 잘 보일 것 같지만, 실제로는 사용자 영향, 재현 조건, 최근 변경, 외부 의존성, 데이터 상태, 브라우저 콘솔, 서버 로그가 뒤섞여 오히려 판단이 느려집니다. AI 로그 트리아지 디버깅 루프는 장애가 터졌을 때 로그를 무작정 늘리는 방식이 아니라, 먼저 증상을 고정하고, 필요한 증거만 수집하고, 가설을 좁힌 뒤, 수정과 롤백을 숫자 기준으로 결정하는 VIBE 코딩 운영 방식입니다.
초보자는 이 루프를 “로그를 보는 순서”로 이해하면 됩니다. 전문가에게는 더 엄격합니다. 로그 레벨, 상관관계 ID, 사용자 세션 범위, 배포 시각, 요청 경로, 오류율, 지연 시간, 재시도 횟수, 큐 적체, 데이터…