Haeminway haeminway
English
기술 노트로
2 분 분량

외부 호출은 가끔 실패한다: 지수 백오프와 재시도 예산

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를 추가하는 이유는?
여러 실행이 실패 후 정확히 같은 시점에 재시도하면 서버가 다시 과부하를 받습니다. 무작위 지터를 더해 재시도가 시간상 분산되도록 합니다.