외부 호출은 가끔 실패한다: 지수 백오프와 재시도 예산
429·503·타임아웃 같은 일시적 실패는 재시도로 살릴 수 있다. 단 멱등성 없는 쓰기는 멱등키 없이 재시도하지 마라. 6분 안에서 멈춰라.
외부·서비스 호출은 일시적으로 실패한다: 재시도를 처음부터 설계에 넣어라. 단, 무엇이든 재시도하면 안 된다. 먼저 작업을 분류한다: 멱등(safe) / 멱등키로 안전 / 비멱등(위험).
왜 중요한가
재시도가 없으면 잠깐의 429 한 번에 작업 전체가 실패한다. 반대로 비멱등 쓰기를 무턱대고 재시도하면 같은 결제·같은 메일이 두 번 나간다. 둘 다 비용이 크다.
일시적 실패만, 백오프로
재시도 대상: 408, 429, 500, 502, 503, 504, 네트워크 타임아웃. 그 외(400, 401, 403)는 재시도해도 똑같이 실패하니 즉시 멈춘다.
function withBackoff(fn, { max = 5 } = {}) {
let wait = 400;
for (let attempt = 1; ; attempt++) {
try {
return fn();
} catch (err) {
if (attempt >= max || !isTransient(err)) throw err;
Utilities.sleep(wait + Math.floor(Math.random() * 200)); // jitter
wait = Math.min(wait * 2, 8000); // 지수 증가, 상한
}
}
}
- 지터(jitter) 를 더해 여러 실행이 같은 순간에 몰리지 않게 한다.
- 최대 경과 시간을 6분 한도 아래로 둔다. 넘을 것 같으면 커서를 저장하고 다음 실행에서 잇는다.
재시도 예산: 호출이 아니라 실행 단위로
위의 max는 한 호출의 상한일 뿐이다. 진짜 사고는 여러 호출이 각자 5번씩 재시도하다 6분을 통째로 태우는 것 — 그러면 정작 처리해야 할 행은 손도 못 댄다. 그래서 재시도는 호출별이 아니라 실행(run) 하나가 공유하는 예산으로 묶는다: 남은 시간과 남은 시도 횟수를 한 곳에서 깎는다.
// 실행 전체가 공유하는 예산: 한 엔드포인트가 6분을 다 쓰지 못하게
const budget = { until: Date.now() + 5 * 60 * 1000, left: 30 };
const canRetry = () => Date.now() < budget.until && budget.left-- > 0;
// withBackoff 안의 `attempt >= max` 검사를 `!canRetry()`로 바꾼다.
예산이 바닥나면 즉시 멈추고 커서를 저장해 6분 천장 다음 실행에서 잇는다. 한 실행이 끝까지 못 가는 건 정상이다 — 다 한 척하며 절반만 처리하는 게 사고다.
비멱등 쓰기는 멱등키로
// 같은 요청이 두 번 와도 한 번만 처리
if (props.getProperty("done:" + idemKey)) return cached;
const result = doWrite();
props.setProperty("done:" + idemKey, "1");
멱등 표시는 PropertiesService에 남기는데, Properties·Cache 한계를 넘지 않게 키를 짧게 쓰고 주기적으로 청소한다. 그리고 동시에 두 실행이 같은 키를 통과할 수 있으니, 돈이 오가는 쓰기는 LockService로 한 번 더 감싼다.
깊이: 부분 실패 정규화
fetchAll로 여러 호출을 묶으면 일부만 실패할 수 있다. 결과를 { ok, status, body, error }로 정규화하고, 전체 실패가 아니라 성공분은 진행 + 실패분만 재시도한다. 로그에는 correlation id를 남기되 payload 원문은 남기지 않는다.
핵심 한 줄: 일시적 실패만 백오프+지터로 재시도하고, 비멱등 쓰기는 멱등키로 막아라.
자주 묻는 질문
- 어떤 HTTP 상태 코드에서만 재시도를 해야 하나요?
- 408, 429, 500, 502, 503, 504와 네트워크 타임아웃처럼 일시적인 실패에만 재시도합니다. 400, 401, 403 같은 클라이언트 오류는 재시도해도 똑같이 실패하므로 즉시 중단해야 합니다.
- 비멱등 쓰기를 재시도하면 왜 위험한가요?
- 같은 결제나 같은 메일이 두 번 실행될 수 있습니다. 멱등키를 사용해 이미 처리된 요청인지 먼저 확인한 뒤에만 쓰기를 수행해야 합니다.
- 지수 백오프에 jitter를 추가하는 이유는?
- 여러 실행이 실패 후 정확히 같은 시점에 재시도하면 서버가 다시 과부하를 받습니다. 무작위 지터를 더해 재시도가 시간상 분산되도록 합니다.