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

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

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