실패 응답은 작업이 저장되지 않았다는 확정 증거가 아닙니다
요청을 보낸 뒤 503을 받았을 때 같은 작업을 다시 보내면 되는지 먼저 확인해야 합니다. 읽기 요청과 새로운 작업을 만드는 요청은 영향이 다릅니다. 특히 서버가 작업을 저장한 뒤 응답 전달 단계에서 문제가 생기면, 사용자는 실패를 봤지만 작업은 이미 하나 생겼을 수 있습니다. 이 상태에서 식별자 없이 같은 생성 요청을 다시 보내면 두 개가 만들어질 수 있습니다. 화면의 실패 표시와 서버의 저장 상태를 같은 사실로 취급하지 않는 것이 출발점입니다.
이번 실험은 실제 결제나 서비스 요청을 하지 않고 로컬 상태 기계로 이 경계를 만들었습니다. 첫 요청에서는 작업을 하나 저장한 뒤 503 응답을 돌려주도록 일부러 설계했습니다. 모든 503이 이런 상태를 뜻한다는 주장이 아닙니다. 원래 결과를 모르는 클라이언트가 재시도할 때 무엇을 확인해야 하는지 보여 주는 실패 가정입니다. 코드에서는 실제 상태를 직접 볼 수 있으므로 응답이 유실된 뒤 남은 작업 수를 대조할 수 있었습니다.
HTTP 의미 규칙은 비멱등 메서드의 자동 재시도를 가볍게 가정하지 말도록 설명합니다. 멱등성이란 같은 의도의 요청을 반복해도 의도한 서버 효과가 달라지지 않는 성질입니다. 단순히 요청 본문이 같거나 일정 시간 기다렸다는 뜻은 아닙니다. 자동화에 재시도 루프를 넣기 전에 해당 요청의 작업 효과와 서버가 제공하는 중복 방지 계약을 먼저 확인해야 합니다.
동일한 가상 요청을 두 번 보내자 작업이 두 개 남았습니다
첫 비교는 요청 키를 사용하지 않았습니다. 가상 본문 invoice-17로 요청하자 TASK-1이 저장됐지만 503이 반환됐습니다. 같은 본문을 다시 보낸 결과 TASK-2가 새로 만들어지고 201이 반환됐습니다. 최종 응답만 보면 두 번째 시도에서 성공한 것으로 보이지만 서비스 안에는 같은 본문의 작업이 두 개 남았습니다. 이것이 재시도를 통해 정상 응답을 얻었다는 결과와 업무가 한 번만 적용됐다는 결과를 분리해야 하는 이유입니다.
두 번째 비교에서는 batch-17이라는 요청 키를 사용했습니다. 서비스는 키와 본문, 성공 결과를 저장한 뒤 첫 응답만 같은 실패 가정에 따라 503으로 돌려줍니다. 다음에 같은 키와 같은 본문을 보내자 저장된 TASK-1의 결과를 다시 반환했습니다. 작업 수는 한 개였습니다. 이 서비스는 실험을 위해 만든 간단한 계약입니다. 특정 업체가 503이나 다른 실패 결과를 저장하는 방식까지 그대로 복제한 것이 아닙니다.
| 방식 | 첫 응답 | 두 번째 응답 | 남은 작업 |
|---|---|---|---|
| 요청 키 없음 | 저장 후 503 | 새 TASK-2, 201 | 2개 |
| 동일 키·동일 본문 | 저장 후 503 | 기존 TASK-1, 201 | 1개 |
키의 이름보다 서버가 무엇을 저장하는지가 중요합니다
요청 헤더에 임의의 식별자를 하나 붙인다고 모든 서버가 중복을 막아 주지는 않습니다. 서버가 그 식별자를 어떤 범위에서 저장하고, 어떤 본문과 연결하며, 얼마나 보관하는지 계약이 있어야 합니다. 사용자별로 구분하는 키인지, 엔드포인트별인지, 같은 키를 동시에 받은 경우에 어떻게 처리하는지도 영향을 줍니다. 이 글의 로컬 서비스는 메모리 사전에 키와 본문, 결과를 함께 저장했습니다. 프로세스가 종료돼도 유지되는 저장소나 동시 요청 잠금은 구현하지 않았습니다.
실제 예로 Stripe의 공식 문서는 멱등 요청 키와 원래 매개변수의 비교, 저장과 재사용 범위를 설명합니다. 이 문서는 그런 서버 계약이 필요함을 확인하는 출처로 사용했습니다. 이번 예제의 내부 실패 처리나 409 상태 코드를 Stripe의 동작이라고 말하지 않습니다. 서로 다른 서비스의 키 보관 기간과 실패 결과 저장 규칙을 하나의 보편적인 값으로 합치면 재시도 정책이 잘못될 수 있습니다. 실제 대상의 문서를 읽고 작업별로 적용해야 합니다.
클라이언트에서도 같은 논리 작업에는 같은 키를 유지해야 합니다. 오류가 날 때마다 새 키를 만들면 서버는 새로운 작업으로 볼 수 있습니다. 반대로 서로 다른 작업에 같은 키를 재사용하면 필요한 작업이 누락되거나 충돌할 수 있습니다. 배치 식별자와 항목 식별자를 어떻게 조합할지, 재시작 뒤에도 같은 작업임을 어떻게 기억할지 기록 설계에서 먼저 정해야 합니다.
같은 키로 본문을 바꾼 요청은 별도 오류로 막았습니다
가상 서비스에 batch-17 키를 유지한 채 본문을 different로 바꿔 보냈습니다. 서비스는 기존 본문과 다르다는 이유로 409를 반환했고 새 작업을 만들지 않았습니다. 이 409는 이번 로컬 서비스에서 선택한 응답입니다. 모든 멱등 API가 같은 상태 코드를 사용한다는 뜻은 아닙니다. 확인한 것은 동일 키를 서로 다른 의도에 섞어 쓰는 상황을 서버가 별도 분기로 처리한다는 점입니다. 이 오류를 일시 장애처럼 반복 요청해서 해결할 대상으로 두지 않았습니다.
본문 일치 기준도 서비스마다 다를 수 있습니다. JSON 필드 순서가 달라졌을 때 같은 내용으로 보는지, 생략과 null을 같은 것으로 보는지, 일부 매개변수만 비교하는지 문서가 필요합니다. 예제는 짧은 문자열을 정확히 비교하므로 이런 정규화 문제를 다루지 않았습니다. 실제 사용에서는 재시작 과정에서 본문을 다시 생성할 때 값이 바뀌지 않는지도 확인해야 합니다. 키만 보존하고 금액이나 대상 ID가 달라지면 같은 작업이라는 전제가 깨집니다.
요청의 구조와 자료형이 바뀌는 상황은 JSON 계약의 버전 이관과 회귀 검사로 이어집니다. 새로운 필드를 추가한 뒤 과거 작업의 키를 그대로 재사용할 수 있는지는 별도의 서버 계약입니다. 재시도는 자료의 유효성 검사를 대신하지 않으며, 잘못된 본문을 같은 키로 여러 번 보내도 올바른 요청으로 바뀌지 않습니다.
429·503의 대기 안내는 초와 날짜 두 형태로 읽었습니다
두 번째 실험 묶음은 실제 대기를 하지 않는 가상 시계입니다. 고정한 시작 시각에 429와 Retry-After 값 2를 받았다고 가정했습니다. 그 뒤 2초가 지난 가상 시점에서 503과 HTTP 날짜 형태의 안내를 받았습니다. 그 날짜는 시작 뒤 6초이므로 추가로 4초 기다린 다음 세 번째 응답 201을 받습니다. 결과에는 각 시도 시각, 응답, 계산한 대기 시간이 들어 있습니다. 가상 경과 시간은 총 6초이며 실제 sleep 시간은 0초입니다.
RFC 9110에서 Retry-After는 지연 초 또는 HTTP 날짜로 나타낼 수 있습니다. RFC 6585의 429는 일정 기간의 요청 과다를 나타내며 대기 안내가 포함될 수 있습니다. 따라서 숫자만 읽는 처리기는 날짜 표현을 놓칠 수 있고, 헤더가 없을 때의 정책도 별도로 필요합니다. 예제는 두 형태가 고정 입력으로 주어진 작은 계산입니다. 잘못된 날짜, 큰 시계 오차, 모든 비표준 헤더를 처리하는 범용 HTTP 클라이언트를 만든 것은 아닙니다.
코드에는 자체 대기 후보도 두되 서버가 알려 준 시간보다 이르게 재시도하지 않도록 더 긴 쪽을 선택했습니다. 재현 가능하게 보이도록 작은 고정 보정값을 사용했으며 무작위 지터를 실제로 시험하지는 않았습니다. 여러 클라이언트의 요청이 동시에 몰리는 운영 환경에서는 분산 대기와 서버 제한을 함께 설계해야 합니다. 한 클라이언트의 작은 가상 시계 결과를 서버의 처리 용량 측정으로 해석하지 않습니다.
대기 시간이 예산보다 길면 짧게 깎지 않고 보류합니다
실험의 총 가상 대기 예산은 10초입니다. 정상 경로와 같은 재시도 함수에 429·Retry-After 120초, 그 뒤 201이라는 별도 응답표를 넣었습니다. 실제 추적에는 첫 시도 한 건만 남았고 requiredWaitSeconds는 120, 중단 이유는 budget exceeded였습니다. 두 번째 201은 소비하지 않았으며 가상 경과도 0초였습니다. 단순히 120이 10보다 크다고 계산한 뒤 중단됐다고 선언한 것이 아니라, 같은 제어 흐름이 다음 시도에 들어가지 않는지 검사했습니다.
이 예제에서는 서버의 120초를 10초로 줄여 다시 요청하지 않습니다. 클라이언트의 최대 예산은 서버의 대기 지시를 무시할 허가가 아니기 때문입니다. 결과 JSON의 excessiveRetryAfter에서 원래 두 응답과 실제 한 건의 trace를 나란히 읽을 수 있습니다. 이후 재개 시점과 작업을 보류한 사실은 별도 원장에 남겨야 하며, 이번 함수는 미래 실행을 예약하거나 상대 작업을 취소하지 않습니다.
시도 횟수와 총 경과 시간도 구분해 두는 편이 좋습니다. 세 번만 재시도하도록 했더라도 각 대기가 매우 길면 작업이 예상보다 오래 남습니다. 반대로 총 시간만 제한하면 짧은 요청이 지나치게 반복될 수 있습니다. 실제 처리기는 각 요청의 시간 제한, 최대 시도 수, 총 예산과 취소 조건을 함께 관리해야 합니다. 이번 코드는 준비한 세 응답을 순회하는 유한한 사례이며, 연결 시간 제한을 실제 네트워크에서 검증한 결과는 아닙니다.
정상 완료와 예산 초과 보류를 같은 성공 상태로 기록하지 마세요. 마지막으로 확인한 서버 작업 ID가 있는지, 최종 상태를 아직 모르는지 원장에 남기면 수동 확인도 쉬워집니다. 단순히 마지막 예외가 없어졌다는 사실보다 작업 효과를 어디까지 확인했는지가 중요합니다. 결과를 모르는 상태에서 새 키로 처음부터 다시 시작하면 중복 방지의 전제도 사라질 수 있습니다.
코드의 재시도 후보 상태는 429와 503으로 한정했습니다. 인증 실패나 본문 검증 오류를 모두 같은 대기 문제로 묶는 구현이 아닙니다. 실제 대상에서 어떤 상태를 다시 시도할 수 있는지는 별도로 확인해야 합니다. 이 예제에 없는 상태를 추가한다면 응답별 기대 행동과 중단 이유를 고정 입력에 함께 넣어야 합니다.
시도 원장에는 응답과 작업 효과를 분리해서 기록합니다
이번 결과는 각 응답 상태와 생성된 작업 목록을 따로 저장합니다. 그래서 첫 503 뒤에 작업이 이미 있었는지, 다음 201이 새 작업을 뜻하는지 기존 결과를 뜻하는지 읽을 수 있습니다. 실제 클라이언트는 서버 내부 목록을 직접 볼 수 없을 수 있습니다. 그런 환경에서는 서버가 제공하는 조회 엔드포인트나 작업 상태 계약을 이용해야 합니다. 조회로 확인할 수 없는 상태를 임의로 실패나 성공으로 확정하지 않는 것이 필요합니다.
원장에는 논리 작업 ID, 요청 키의 안전한 참조값, 본문 버전, 시도 번호, 응답 상태, 서버 작업 ID, 다음 조치 등을 나눠 넣을 수 있습니다. 인증 토큰과 민감한 본문을 공개 로그에 그대로 쓰지는 않습니다. 가상 자료로 먼저 경계를 확인하는 방법은 개인정보 없이 자동화 시험 자료를 만드는 가이드에서 이어 볼 수 있습니다. 이번 공개 파일도 실제 주문이나 계정을 대신해 새 문자열만 사용했습니다.
같은 요청 키가 보인다는 사실만으로 여러 시도가 같은 작업이었다고 단정하지 말고 본문과 서버 계약을 함께 확인하세요. 특히 자동화가 배치를 여러 조각으로 나누면 어느 항목까지 반영됐는지 알아야 재시작 범위를 정할 수 있습니다. 요청 단위의 중복 방지와 배치 전체의 원자성은 다른 문제입니다. 하나의 키가 모든 단계의 저장과 외부 알림을 한꺼번에 보호한다고 가정하지 않습니다.
이 실험의 통과는 운영 서비스의 멱등성을 인증하지 않습니다
실험은 두 개의 로컬 서비스 인스턴스와 고정 응답표, 가상 시계로 실행했습니다. 동시 요청, 프로세스 재시작, 키 만료, 영속 저장소 장애나 실제 서비스의 실패 결과 정책을 검증하지 않았습니다. 키가 없는 비교는 중복이 생기는 모양을 의도적으로 보여 주기 위한 것이며 권장 재시도 구현이 아닙니다. 재시도를 추가하기 전에 서버가 같은 작업을 어떤 기준으로 묶는지 확인해야 한다는 결론으로 읽어야 합니다.
직접 재현할 때는 최종 응답보다 두 작업 목록의 길이를 먼저 보세요. 그다음 같은 키와 다른 본문이 어디에서 차단되는지, 가상 대기 6초와 실제 sleep 0초가 어떻게 구분돼 있는지 확인하면 됩니다. 실제 대상에 적용하려면 그 서비스의 멱등 계약과 대기 안내, 총 예산을 별도로 문서화하세요. 더 많이 재시도하는 것보다 모르는 결과를 추측하지 않고 추적할 수 있게 만드는 것이 우선입니다.
입력과 결과를 직접 대조하는 재현 자료
실험 실행일은 2026년 10월 15일이며 이 글의 발행 일정과 구분합니다. Python 3 표준 라이브러리로 가상 자료만 처리했습니다. 아래 파일은 기존 사용자 파일을 인자로 받지 않습니다.
python3 retry-idempotency-local-fixture-lab-20261015.py실행 시각과 런타임은 환경에 따라 달라집니다. 판정 근거는 다운로드 결과의 고정 입력과 각 사례의 실제 출력으로 확인하세요.
규칙을 확인한 공식 원문
아래 원문은 2026년 10월 15일 확인했습니다. 형식과 동작의 설명을 확인한 출처이며, 이 글의 가상 실험이나 실제 업무 성과를 대신 증명하지 않습니다.
- HTTP Semantics: Retry-After — 대기 지시의 지연 초·HTTP 날짜 표현과 503의 의미를 확인했습니다.
- RFC 6585: 429 Too Many Requests — 429와 선택적인 Retry-After 안내를 확인했습니다.
- Stripe API: Idempotent requests — 서버의 키 저장·동일 매개변수 계약이 필요함을 확인했습니다. 이 글의 가상 서비스는 Stripe 구현을 재현한 것이 아닙니다.



