외부 호출은 가끔 실패한다: 지수 백오프와 재시도 예산
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분 한도 아래로 둔다. 넘을 것 같으면 커서를 저장하고 다음 실행에서 잇는다.
비멱등 쓰기는 멱등키로
// 같은 요청이 두 번 와도 한 번만 처리
if (props.getProperty("done:" + idemKey)) return cached;
const result = doWrite();
props.setProperty("done:" + idemKey, "1");
깊이: 부분 실패 정규화
fetchAll로 여러 호출을 묶으면 일부만 실패할 수 있다. 결과를 { ok, status, body, error }로 정규화하고, 전체 실패가 아니라 성공분은 진행 + 실패분만 재시도한다. 로그에는 correlation id를 남기되 payload 원문은 남기지 않는다.
핵심 한 줄: 일시적 실패만 백오프+지터로 재시도하고, 비멱등 쓰기는 멱등키로 막아라.
자주 묻는 질문
- 어떤 HTTP 상태 코드에서만 재시도를 해야 하나요?
- 408, 429, 500, 502, 503, 504와 네트워크 타임아웃처럼 일시적인 실패에만 재시도합니다. 400, 401, 403 같은 클라이언트 오류는 재시도해도 똑같이 실패하므로 즉시 중단해야 합니다.
- 비멱등 쓰기를 재시도하면 왜 위험한가요?
- 같은 결제나 같은 메일이 두 번 실행될 수 있습니다. 멱등키를 사용해 이미 처리된 요청인지 먼저 확인한 뒤에만 쓰기를 수행해야 합니다.
- 지수 백오프에 jitter를 추가하는 이유는?
- 여러 실행이 실패 후 정확히 같은 시점에 재시도하면 서버가 다시 과부하를 받습니다. 무작위 지터를 더해 재시도가 시간상 분산되도록 합니다.