API 시간 초과는 호출자가 응답을 기다리다 멈췄다는 뜻이다. 서버에서 일이 끝났는지는 별도 문제다. 재시도를 결정하려면 같은 작업을 반복해도 되는지, 전체 마감까지 시간이 남았는지, 장애 중인 서비스에 추가 요청을 보낼 여력이 있는지를 함께 확인해야 한다.
응답이 없는데 주문은 생성됐다면
주문 생성 API를 호출한 뒤 800ms가 지나 시간 초과가 났다. 화면은 실패를 알렸지만 서버는 700ms에 주문을 저장했고, 응답만 늦었을 수 있다. 사용자가 다시 누르거나 앱이 자동 재시도하면 같은 주문이 하나 더 생길 수 있다. 반대로 서버까지 요청이 도착하지 않았을 수도 있다. 응답을 받지 못했다는 사실만으로 이 둘을 구별할 수 없다. 이 주문 사례와 뒤의 수치는 설명을 위한 가정이다.
이때 “세 번 재시도”라는 설정은 답이 되지 않는다. 먼저 같은 작업을 다시 보내도 되는지를 알아야 한다. 그다음에는 재시도가 성공할 시간과 서버 여력이 있는지 따져야 한다. 이 글은 그 판단을 효과·시간·부하로 나누어, API 호출 정책에 옮길 수 있게 설명한다.
첫 판단: 같은 요청을 보내도 같은 업무인가
HTTP 의미 규격 RFC 9110 §9.2.2는 같은 요청을 반복했을 때 서버에 의도한 효과가 한 번 수행한 것과 같은 성질을 멱등성이라고 정의한다. PUT·DELETE와 안전한 메서드는 이 성질을 갖는다. 응답 코드까지 같아야 한다는 뜻은 아니다. 삭제가 처음에는 성공하고 다음에는 이미 없는 대상으로 응답하더라도 의도한 효과는 같을 수 있다.
POST라고 무조건 재시도가 불가능한 것도 아니다. 해당 API가 멱등성을 보장하거나 첫 요청이 적용되지 않았음을 확인할 수 있는지가 중요하다. 같은 RFC는 이런 근거 없이 비멱등 요청을 자동 재시도하지 않도록 권고한다. HTTP 메서드 이름만 바꾸어 서버의 중복 처리를 해결할 수는 없다.
주문 API가 호출자 제공 요청 ID를 지원한다고 가정하자. 최초 주문 의도에 order-request-17을 붙이고, 응답을 잃은 재시도에서도 같은 ID와 같은 업무 입력을 보낸다. 시도마다 새 ID를 만들면 서버에는 새 주문으로 보인다. 실제로 두 번째 주문을 하려는 사용자의 행동에는 다른 ID가 필요하다. 입력 내용의 해시만으로 동일 의도라고 단정하면 같은 상품을 두 번 주문하려는 행동을 잘못 합칠 수 있다.
AWS의 멱등 API 설계 원문은 호출자 요청 ID, 동일 ID의 다른 입력, 늦게 도착한 요청과 보관 기간을 다룬다. 이 글의 주문 예시는 그 문제를 설명하는 설계안이며 구현된 주문 시스템이 아니다. 도입할 API에서는 ID의 유효 범위·보관 기간·진행 중 중복의 응답·입력 불일치 처리를 확인해야 한다. 외부 배송·메일까지 그 계약에 포함되는지도 따로 본다.
| 받은 결과 또는 상황 | 다음 행동을 정할 기준 |
|---|---|
| 상태 변경 요청의 응답 유실, 멱등 계약 없음 | 새 요청보다 기존 결과 조회·대사 경로를 우선한다. 결과를 모르면 미확인 상태를 유지한다. |
| 반복 효과가 안전한 작업의 일시적 연결 오류 | 남은 시간·시도 수·부하 예산을 통과하면 재시도 후보로 삼는다. |
| 입력 형식·업무 조건이 잘못됐다는 확정 응답 | 같은 입력을 반복하지 않고 수정하거나 사용자 판단으로 돌린다. |
| 429 또는 일시적 503 | API 계약과 대기 안내를 읽는다. 상태 코드만으로 반복 실행의 안전성을 확정하지 않는다. |
| 사용자가 취소했거나 전체 마감 도달 | 새 시도를 시작하지 않고 정리한다. 이미 처리된 업무의 취소 여부는 별도로 확인한다. |
이 표는 오류 코드별 만능 목록이 아니다. 예컨대 500 응답도 부수 효과가 전혀 없었다는 증거는 아니다. 상태를 바꾼 뒤 응답 생성에서 오류가 났을 가능성을 API 계약과 함께 살펴야 한다.
두 번째 판단: 한 번의 timeout과 전체 마감은 다르다
timeout=800ms가 무엇을 덮는지부터 확인한다. 연결 풀 대기, DNS, TCP·TLS 연결, 요청 전송, 응답 대기와 본문 수신 중 일부만 제한하는 설정일 수 있다. AWS의 타임아웃 해설 원문 PDF는 DNS나 TLS가 제한에서 빠지는 구현을 주의점으로 든다. 타이머가 짧아도 그 바깥의 대기에는 한도가 없을 수 있다.
개별 시도의 제한과 함께 사용자 작업 전체에 deadline, 즉 더 기다리지 않을 마감을 둔다. 2.5초짜리 작업이라면 첫 요청 시점에 마감을 고정하고, 재시도한다고 2.5초를 새로 부여하지 않는다. gRPC deadline 문서도 기간인 timeout과 시점인 deadline을 구분하고, 하위 호출로 전파할 때 이미 쓴 시간을 차감하는 방식을 설명한다.
다음은 이 글의 가상 정책이다. 보편적인 권장 시간값이 아니다.
- 작업 전체 한도: 2,500ms. 최종 응답·정리에 200ms를 남긴다.
- 개별 호출 상한: 800ms. 최초 호출을 포함한 총 시도 수는 최대 3회다.
- 첫 실패 뒤 대기: 100ms, 두 번째 실패 뒤 대기: 300ms로 고정해 계산한다.
- 호출을 새로 시작하려면 최소 400ms가 남아 있어야 한다고 정한다.
| 구간 | 가상 시각 | 남은 판단 |
|---|---|---|
| 첫 시도 시간 초과 | 0 → 800ms | 전체 마감은 여전히 2,500ms |
| 첫 대기 | 800 → 900ms | 대기도 예산을 쓴다 |
| 두 번째 시도 시간 초과 | 900 → 1,700ms | 정리 시간을 뺀 여유는 600ms |
| 세 번째 시도 검토 | 1,700ms | 300ms 대기 뒤 실행에 300ms만 남음 |
| 중단 | 1,700ms | 최소 400ms 조건을 못 채워 대기·호출을 시작하지 않음 |
시도 수만 보면 한 번이 남았지만 시간 조건은 통과하지 못했다. 무조건 세 번 시도한다면 800 × 3 + 100 + 300 + 200 = 3,000ms가 되어 전체 한도를 넘는다.
최소 400ms는 성공을 보장하는 값이 아니라 남은 시간이 너무 짧으면 새 일을 시작하지 않겠다는 정책이다. 최소값을 100ms로 바꾸면 2,000ms에 세 번째 시도를 시작하고, 그 시도의 상한을 300ms로 줄여 2,300ms에 종료할 수 있다. 빨리 끝날 가능성이 있는 작업에는 이 선택이 유효할 수 있다. 반대로 반드시 500ms 정도 걸리는 작업이라면 낭비에 가깝다. 실제 분포와 업무 요구를 보고 선택한다.
설계식은 다음과 같다. 모든 값의 단위는 같아야 한다.
새 시도의 가능 시간 = 전체 마감 − 현재 시각 − 정리 여유
다음 대기 + 최소 실행시간 > 가능 시간 → 중단
대기 뒤 시도 상한 = min(개별 시도 상한, 남은 가능 시간)
실제 코드에서는 대기 전에 계산한 값만 믿지 말고, 깨어난 뒤와 전송 직전에 취소·남은 시간을 다시 검사한다. 프로세스 내부의 경과 시간에는 단조 시계를 쓰고, 서로 다른 서버의 단조 시계 값을 그대로 전달하지 않는다. 하위 프로토콜의 남은 시간 전파 규약을 사용해야 한다. 위 시간표는 지연 없이 정시에 깨어나는 가상 모델이며 운영체제 스케줄링까지 보장하는 실측이 아니다.
backoff와 jitter는 언제 다시 모이는지를 바꾼다
실패하자마자 반복하면 서버가 회복할 틈이 줄어든다. backoff는 재시도 사이에 대기를 두는 방식이다. 지수 backoff는 실패할수록 대기 창을 키우고 상한을 둔다. 하지만 동시에 실패한 호출자가 모두 200ms, 다음에는 400ms를 기다리면 다시 같은 때에 몰릴 수 있다.
jitter는 그 대기 시각을 분산한다. AWS의 Exponential Backoff And Jitter는 일정한 지수 대기에도 호출 무리가 남는 현상과 여러 jitter 방식을 시뮬레이션으로 비교했다. 원문의 개선 수치를 우리 서비스의 성능으로 가져오지는 않는다.
대표적인 Full Jitter를 다음처럼 표현할 수 있다. k=0은 첫 번째 재시도 전이며 b는 처음 대기 창, C는 창의 상한이다.
대기 창 W_k = min(C, b × 2^k)
대기 시간 J_k = 0부터 W_k 사이에서 균등하게 선택
앞 시간표의 100ms·300ms는 b=200ms일 때 첫 창 200ms, 둘째 창 400ms 안에서 골랐다고 가정한 값이다. 다운로드 예제에서는 검증을 재현할 수 있게 이 값을 직접 넣는다. 난수 품질이나 실제 분산 정도를 시험하지 않는다.
직관을 위해 여섯 호출자가 모두 한 번 재시도한다고 생각하자. 모두 300ms를 기다리면 그 순간에 여섯 건이 온다. 설명용으로 20·70·120·170·220·270ms를 고르면 세 개의 100ms 구간마다 두 건씩 들어간다. 총 추가 호출은 어느 경우든 여섯 건이다. 이 배열은 손으로 정한 예시이며 jitter의 평균 성과나 충돌 방지를 보장하는 실험이 아니다. 대기 분산과 호출 총량 제한은 서로 다른 일이다.
Retry-After가 남은 시간보다 길다면
429를 정의하는 RFC 6585 §4는 요청이 너무 많을 때 Retry-After가 포함될 수 있다고 명시한다. 반드시 있는 헤더는 아니다. RFC 9110 §10.2.3의 값은 응답 수신 후 기다릴 정수 초 또는 HTTP 날짜다. Retry-After: 2는 2ms가 아닌 2초다.
이 글의 정책에서는 유효한 서버 대기를 하한으로 삼고 그 뒤 jitter를 더한다. 서버 대기 500ms에 선택한 jitter 100ms라면 600ms 뒤를 검토한다. 이는 설명용 선택이며 모든 SDK가 같은 공식을 쓰지는 않는다. SDK가 이미 처리하는지 먼저 확인한다.
앞의 800ms 시점에 Retry-After: 2를 받았다면 2초 대기만 해도 2,800ms다. 이번 요청의 2.5초 마감에는 들어가지 않는다. 서버 대기를 남은 시간으로 잘라 더 일찍 재요청하지 않고 이번 동기 요청을 종료한다. 나중에 이어야 하는 업무라면 별도 작업 저장·수명·사용자 안내가 필요하다. 단순히 백그라운드로 빼는 것으로 보장되지는 않는다.
HTTP 날짜 형식은 클라이언트 시계와 과거 시각 처리도 고려해야 한다. 파싱 실패·부재 시 대체 정책 역시 명시한다. 제공 예제는 파싱이 끝난 비음수 상대 대기를 입력받으며 HTTP 날짜 파서나 SDK 호환 구현을 제공하지 않는다.
세 번째 판단: 요청 하나의 세 번과 서비스 전체의 예산
앱이 SDK를 세 번 호출하고, SDK가 매번 세 번 시도하고, 그 아래 계층도 세 번 시도하면 어떻게 될까. 각 계층의 시도가 모두 실패하고 상한까지 간다는 가정에서는 맨 아래에 3 × 3 × 3 = 27회 호출이 도달한다. 여기서 각 3은 최초 호출을 포함한 총 시도 수다. deadline이나 다른 중단 장치가 먼저 작동하면 실제 횟수는 줄어든다.
재시도를 책임지는 계층을 한 곳으로 정하고 나머지는 한 번씩만 전달하면 같은 단순 모델에서 맨 아래는 최대 3회다. 다만 무조건 가장 바깥이 정답은 아니다. 전체 업무를 다시 시작하면 이미 끝낸 비싼 단계까지 반복할 수 있다. 오류 의미·멱등 계약·남은 시간·다른 계층의 실제 설정을 아는 곳에 책임을 둔다.
중첩을 없애도 요청이 많으면 부하는 남는다. 원래 요청 1,000건에 각각 총 3회까지 허용하면 최대 3,000회다. 여기에 별도 공유 재시도 예산 100회를 두면 같은 유한 구간에서 최초 1,000회와 추가 100회, 합계 1,100회까지로 제한할 수 있다. 이 계산은 그 구간에서 예산이 보충되지 않고, 모든 재시도가 같은 예산을 거친다는 가정이다. 성공률·초당 처리량 계산은 아니다.
운영에서는 예산의 공유 범위와 보충 규칙을 정해야 한다. 인스턴스마다 100회라면 서버 10대에서 총 100회가 아니다. 의존 서비스나 테넌트별로 나눌지도 결정한다. 예산 소진 시 즉시 실패시킬지, 저장된 작업으로 넘길지 역시 업무 정책이다. 최초 요청 자체가 과도하면 재시도 예산만으로 해결되지 않으므로 요청 수용 제한과 동시 실행 한도도 필요하다.
취소는 다음 시도를 멈추지만 이미 끝낸 일을 되돌리지는 않는다
첫 시도가 800ms에 끝나고 100ms 대기 중인데, 사용자가 850ms에 화면을 닫아 작업을 취소했다고 하자. 취소를 관찰하는 대기라면 900ms에 두 번째 요청을 보내지 않아야 한다. 실행 중인 호출에도 취소 신호를 연결하고, 작업이 만든 자원을 정리해야 한다.
gRPC 공식 설명은 RPC 취소 이후 서버 애플리케이션이 만든 작업을 멈추는 책임을 애플리케이션에 둔다. 호출이 취소됐다는 신호만으로 DB 쓰기나 외부 업무가 자동 롤백되지는 않는다. 주문 생성의 결과가 미확인이라면 취소 뒤에도 그 결과를 확인할 경로가 필요하다.
화면을 만든다면 “실패” 하나로 뭉개지 않는 편이 낫다. 확정 거절은 입력 수정으로, 처리 미확인은 결과 조회로, 대기 예산 소진은 나중에 다시 시도할 안내로 연결한다. 이는 UI 설계 제안이며 이 글의 예제에 구현된 앱 화면은 아니다.
가상 시계로 중단 조건을 직접 바꿔 보기
표준 라이브러리만 사용한다. 저장한 파일이 있는 디렉터리에서 다음을 실행한다. 네트워크 호출과 실제 sleep 없이 가상 시각을 더하며, 고정 입력의 결과를 단언문으로 대조한다.
python3 retry-budget-lab.py
11개 시나리오 중 핵심 결과는 다음과 같다. 경과 시각은 정리용 200ms를 실제 실행한 값이 아니라, 호출·대기 모델이 멈춘 시점이다.
| 조건 | 모델이 시작한 시도 | 종료 시각·이유 |
|---|---|---|
| 최소 실행시간 400ms, 계속 시간 초과 | 2회 | 1,700ms, 다음 시도 시간 부족 |
| 최소 실행시간 100ms로 변경 | 3회 | 2,300ms, 마지막 시도 상한 300ms |
| 첫 시간 초과 뒤 두 번째가 250ms에 성공 | 2회 | 1,150ms, 성공 |
| 같은 효과 보장이 없는 시간 초과 | 1회 | 800ms, 결과 확인 필요 |
| 800ms에 서버 대기 2초를 받음 | 1회 | 800ms, 마감 안에 다음 시도 불가 |
| 첫 실패 뒤 재시도 토큰이 없음 | 1회 | 800ms, 부하 예산 소진 |
| 첫 재시도 대기 중 850ms에 취소 | 1회 | 850ms, 새 시도 없음 |
나머지는 확정 오류, 전송 전 취소, 시도 횟수 상한, 서버 대기 하한을 확인한다. 중첩 호출 수와 위 산술도 별도로 검사한다. 이 결과는 가상 시간 모델의 분기 검증이다. 실제 네트워크 타임아웃·동시 토큰 획득·서버 작업 취소·HTTP 헤더 파싱·성능 향상을 시험하지 않았다.
자기 서비스에 적용할 때는 먼저 코드의 minimum, deadline, budget을 바꾸어 어느 조건에서 호출이 사라지는지 본다. 그다음 실제 클라이언트의 연결·수신 제한과 취소 전파를 별도 환경에서 시험한다. 운영에 복사할 범용 HTTP 재시도 라이브러리로 사용하기에는 빠진 경계가 많다.
설정값보다 먼저 남길 한 장의 정책
다음 양식은 팀에서 재시도를 검토할 때 복사해 쓸 수 있다. 값이 미확인이면 빈칸을 추정으로 채우지 않는다.
작업 / 의존 API:
반복 실행의 근거: 읽기 / 멱등 계약 / 첫 요청 미적용 확인 / 미확인
같은 작업 ID의 범위·보관 기간 / 결과 조회 방법:
재시도 후보 오류 / 즉시 종료 오류:
최초 요청 포함 최대 시도 수 / 재시도 담당 계층:
전체 deadline / 개별 시도 상한 / 타이머가 덮는 단계:
최소 실행시간 / 정리 여유:
backoff 창·상한 / jitter / Retry-After 처리:
공유 재시도 예산의 범위·보충 규칙 / 동시 실행 한도:
취소 전파 / 결과 미확인 시 사용자에게 보일 다음 행동:
실제 SDK 설정·버전 / 검증하지 않은 조건:
배포 뒤에는 최종 성공률만 보지 않고 원래 요청 수, 추가 시도 수, 중단 이유와 전체 경과 시간을 함께 본다. 재시도 뒤 성공한 수가 늘어도 지연과 하위 부하가 감당 가능한지 비교해야 한다. 상태 코드·시도 순번 같은 제한된 진단 정보를 남기되 주문 내용이나 민감한 요청 본문을 관측 목적으로 복제할 필요는 없다.
어느 구간에서 시간을 쓰는지부터 모른다면 AI 요청의 지연을 나누는 방법으로 이어 읽을 수 있다. DB 저장 뒤 후속 작업을 잃지 않는 구조가 필요하면 트랜잭셔널 아웃박스가 다음 질문이다. 이 정책 양식을 다시 점검할 때는 글 저장 기능을, 후속 글을 받아볼 때는 RSS를 사용할 수 있다.
- 시간 초과 뒤에는 실패 확정과 결과 미확인을 구별한다. 상태 변경 요청은 API의 멱등 계약이나 결과 조회 경로가 먼저다.
- 전체 마감은 최초 요청부터 고정한다. 대기·연결·각 시도·마무리 시간이 같은 예산을 쓴다.
- backoff와 jitter는 재요청 시점을 조절한다. 총 시도 수·공유 재시도 예산·동시 실행 한도는 별도다.
- SDK와 앱의 중첩 재시도를 확인하고, 취소나 예산 소진 뒤 새 시도가 시작되지 않게 한다.
근거와 원자료
자료의 발행 시점과 이번 글에서 참고한 범위를 함께 기록합니다.
- RFC 9110: HTTP Semantics ↗IETF / RFC Editor · 2022
§9.2.2 멱등 메서드와 재시도 조건, §10.2.3 Retry-After 초/날짜. 고정 RFC 원문.
- RFC 6585: Additional HTTP Status Codes ↗IETF / RFC Editor · 2012
§4 429와 선택적인 Retry-After. 제품별 오류 분류·재시도 정책을 보장하지 않음.
- Timeouts, retries, and backoff with jitter ↗Marc Brooker · Amazon Builders’ Library · 2019
공식 PDF, pp.2–5 타임아웃 경계·중첩 재시도·부하·jitter. HTML 로딩 화면 대신 원문 직접 확인. SDK 현재 기본값·실측 근거 아님.
- Making retries safe with idempotent APIs ↗Malcolm Featonby · Amazon Builders’ Library · 상시 제공 문서
호출자 요청 ID·원자적 처리·늦은 요청·같은 ID의 다른 입력. 웹 확인 시점 snapshot이며 주문 시스템을 구현하지 않음.
- Exponential Backoff And Jitter ↗Marc Brooker · AWS Architecture Blog · 2015 / 2023 갱신
Full Jitter 및 일정한 backoff의 군집 현상. 저자 OCC 시뮬레이션을 자체 서비스의 성능 측정으로 전용하지 않음.
- Deadlines ↗gRPC Authors · 2025-07-07 수정 표기
Timeout/deadline 구분, 하위 전파 시 경과시간 차감, 취소 후 애플리케이션 작업 중단 책임. 언어별 전파 기본값 차이; 실제 gRPC 실행 아님.
갱신 기록
첫 원고. 공식 문헌과 유한 가상 시계 예제를 구분하고 재시도 판단표·정책 양식을 제공한다.
오류를 발견했다면 →