HTTP 재시도는 상태 코드 숫자를 보고 요청을 반복하는 기능이 아니라, 원본이 실행됐을 가능성과 같은 논리 작업을 다시 보내도 효과가 한 번만 남는지를 판단하는 계약입니다. 메서드의 safe·idempotent 의미, 서버의 멱등 키, 전체 deadline과 하위 호출 예산을 한 경로에서 함께 결정해야 합니다.
GET 안전성과 관찰적 부수 효과의 구분
상품 조회 GET이 접근 로그와 통계 카운터를 남겨도, 클라이언트가 요청한 의미가 조회라면 안전한 메서드의 취지와 충돌하지 않습니다. 그러나 GET 링크에 보상 지급·삭제를 넣으면 크롤러·미리 가져오기·링크 검사·새로고침이 의도하지 않은 변경을 만들 수 있습니다.
안전성은 클라이언트가 요청한 의미가 상태 변경을 요구하는지이고, 멱등성은 동일 요청을 반복한 의도된 최종 효과가 한 번과 같은지입니다. DELETE는 두 번째 응답이 404로 달라도 의도된 삭제 효과가 같을 수 있습니다. 응답 문자열의 동일성과 멱등성은 다릅니다.
메서드·캐시·기밀성의 분리
| 선택 | 말해 주는 것 | 말해 주지 않는 것 |
|---|---|---|
| GET | 표현 조회의 안전·멱등 의미 | 항상 공개 캐시 허용 |
| POST | 대상이 본문을 처리하는 요청 | 기본적으로 중복 방지·기밀성 |
| HTTPS | 전송 구간 보호 | 서버 로그·브라우저 저장 보호 |
| Cache-Control | 저장·재사용 정책 | 현재 사용자 인가 |
민감값을 POST 본문에 넣으면 URL 기록보다 노출 경로를 줄일 수 있지만 암호화되는 것은 아닙니다. 로그·trace·오류 보고·개발자 도구에서 본문이 남을 수 있습니다. TLS·마스킹·저장 제한·권한을 함께 적용합니다.
POST 응답도 명시적인 HTTP 캐시 조건에서 재사용 가능할 수 있지만 일반 캐시의 지원은 확인해야 합니다. GET이라고 무조건 캐시되고 POST라고 절대 캐시되지 않는다는 규칙으로 설계하지 않습니다.
상태 코드와 실행 결과 불확정성
| 결과 | 알려 주는 신호 | 주의할 불확정성 |
|---|---|---|
| 429 | 요청 제한 | 즉시 반복하면 제한·부하 증폭 |
| 503 | 일시 서비스 불가 가능성 | 모든 구현에서 미실행을 보장하지 않음 |
| 504 | gateway가 제때 응답 못 받음 | 원본은 이미 커밋했을 수 있음 |
| 연결 오류 | 응답을 못 얻음 | 전송 전 실패인지 응답 유실인지 구분 필요 |
주문 요청이 gateway에서 504를 받았다고 하겠습니다. gateway가 응답을 못 받은 것뿐이라 원본이 아직 실행되지 않았는지, 이미 커밋하고 응답만 잃었는지는 이 상태 코드만으로 알 수 없습니다. 상태 trace를 client sends key=K → origin commits order → gateway returns 504 → client retries key=K로 적으면, 재시도 전 먼저 해야 할 일이 새 주문 생성이 아니라 K의 결과 조회임을 알 수 있습니다. 결과가 아직 확정되지 않았고 서버 계약이 안전한 반복을 보장할 때만 같은 K로 다시 보내며, 새 K를 만들면 동일 사용자의 한 주문이 두 논리 작업으로 갈라집니다.
그림을 글로 읽기
- 응답 실패·timeout
- 효과와 요청 계약 확인: 멱등 메서드 · 요청 키 · 불확정 결과 조회
- 기한·시도·부하 예산
- 제한된 재시도 또는 종료
- 응답 실패·timeout → 효과와 요청 계약 확인: 실행 가능성 판단
- 효과와 요청 계약 확인 → 기한·시도·부하 예산: 안전하게 재실행 가능
- 기한·시도·부하 예산 → 제한된 재시도 또는 종료: 남은 시간 내 선택
동일 주문의 동일 논리 키
POST 주문 생성에 멱등성 키를 제공한다면 사용자·작업 범위·본문 fingerprint를 함께 기록해야 합니다. 새 네트워크 시도마다 새 키를 만들면 같은 주문이 다른 요청처럼 실행됩니다. 반대로 본문이 같은 별도 정상 주문을 같은 키로 합치면 주문을 잃습니다.
키 기록과 실제 변경은 같은 원자 경계에 있어야 하고, 같은 키에 다른 인자는 거절합니다. 기록이 만료된 뒤 늦은 재시도가 오면 중복 방지가 사라질 수 있으므로 보관 기간과 오래된 요청 거절·원장 조회 정책을 정합니다. 메서드를 POST로 정했다는 사실만으로 이 계약이 생기지는 않습니다.
Retry-After와 전체 기한의 결합
Retry-After는 지연 시간 또는 HTTP 날짜로 주어질 수 있습니다. 날짜 형태는 시계 차이와 파싱 실패를 고려해야 합니다. 지시된 대기가 사용자의 남은 기한을 넘으면 무한히 기다리기보다 미완료·재시도 가능 상태를 명시합니다.
onRetryableFailure(request, attempt, deadline):
require request.has_safe_repeat_contract
delay = combine_retry_after_and_bounded_backoff_with_jitter()
if attempt >= MAX_ATTEMPTS or now()+delay+nextAttemptBudget > deadline:
return final_failure_or_uncertain_status
wait(delay)
return resend_same_logical_request()
상위 3회·하위 3회 재시도가 겹치면 최대 9회 호출이 될 수 있습니다. 원본 사용자 요청 ID와 시도 ID를 구분하고 전체 예산을 조정합니다. 거절률과 최종 성공률뿐 아니라 실제 하위 호출량·회복 시간을 함께 봐야 합니다.
실패 후 상태 보존 검증
응답만 유실된 주문, 적용 전 명시 거절, gateway timeout, Retry-After가 기한보다 긴 경우를 분리합니다. 반복 GET에는 의도한 지급·삭제가 없고, POST 재전송에는 같은 요청 키의 효과가 한 번만 남아야 합니다.
로그·미리보기·캐시 재검증이 어떤 메서드를 호출하는지도 확인합니다. 재현 연습에서는 응답 유실, 명시적 4xx 거절, 429의 긴 Retry-After, 503/504 뒤 이미 커밋된 경우를 분리합니다. 예상 결과는 safe한 조회는 반복해도 업무 변경이 없고, 주문 POST는 동일 키의 결과가 한 건으로 수렴하며, deadline을 넘는 경우 성공을 추측하지 않고 미확정 상태를 반환하는 것입니다.