Files
2nd/10_Wiki/Topics/Coding/Backend_Retry_Strategy.md
T
2026-05-09 21:08:02 +09:00

4.6 KiB
Raw Blame History

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
backend-retry-strategy Backend 재시도 전략 — 지수 백오프 + Jitter Coding draft B conceptual 2026-05-09 2026-05-09
backend
resilience
retry
backoff
vibe-coding
language applicable_to
TypeScript / 모든 언어
Backend
Worker
exponential backoff
jitter
retry budget
idempotency-aware retry

Backend 재시도 전략

무조건 재시도 = thundering herd. 지수 백오프 + jitter + 멱등 보장 + 재시도 예산 4가지 모두 있어야 production-grade. 그리고 재시도 가능 에러만 재시도.

📖 핵심 개념

  • 지수 백오프: 1s → 2s → 4s → 8s, 상한 (보통 30s).
  • Jitter: 랜덤 분산 — 모든 클라이언트가 동시 재시도 방지.
  • Retry budget: 짧은 시간에 N번 초과면 즉시 fail.
  • 멱등성: 재시도 안전한 연산만 재시도.

💻 코드 패턴

기본 retry with backoff + full jitter

async function withRetry<T>(
  op: (attempt: number) => Promise<T>,
  opts: {
    maxAttempts?: number;
    baseMs?: number;
    capMs?: number;
    isRetryable?: (e: unknown) => boolean;
  } = {}
): Promise<T> {
  const { maxAttempts = 5, baseMs = 200, capMs = 30_000,
          isRetryable = defaultRetryable } = opts;
  let lastErr: unknown;
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await op(attempt);
    } catch (e) {
      lastErr = e;
      if (!isRetryable(e) || attempt === maxAttempts - 1) throw e;
      // Full jitter — Math.random * exp
      const exp = Math.min(capMs, baseMs * Math.pow(2, attempt));
      const delay = Math.random() * exp;
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw lastErr;
}

function defaultRetryable(e: any): boolean {
  if (e?.code === 'ECONNRESET' || e?.code === 'ETIMEDOUT') return true;
  if (e?.response?.status >= 500) return true;
  if (e?.response?.status === 429) return true; // rate limited
  return false;
}

Idempotency-Key 와 결합

const key = crypto.randomUUID();
await withRetry(() =>
  fetch('/api/payments', {
    method: 'POST',
    headers: { 'Idempotency-Key': key, 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  }).then(checkStatus)
);

같은 key 로 재시도 → 서버가 중복 처리 안 함.

Retry-After 헤더 존중

isRetryable: (e) => {
  const status = e?.response?.status;
  if (status === 429 || status === 503) {
    const retryAfter = e?.response?.headers?.['retry-after'];
    if (retryAfter) {
      // Retry-After 가 있으면 그 시간 + jitter
      // (위 backoff 무시하고 별도 delay 계산)
    }
    return true;
  }
  return status >= 500 && status < 600;
}

Retry budget (token bucket)

class RetryBudget {
  private tokens: number;
  constructor(private capacity = 100, private refillPerSec = 10) {
    this.tokens = capacity;
  }
  tryAcquire(): boolean {
    // refill ... 단순화
    if (this.tokens < 1) return false;
    this.tokens--;
    return true;
  }
}
const budget = new RetryBudget();

await withRetry(op, {
  isRetryable: (e) => defaultRetryable(e) && budget.tryAcquire(),
});

🤔 의사결정 기준

에러 종류 재시도
5xx (서버)
429 + Retry-After — Retry-After 존중
408 timeout
4xx (400/401/403/404) — 클라이언트 잘못
network ECONNRESET / ETIMEDOUT
validation 실패
사용자 cancel
작업 종류 재시도 적합
GET 조회
멱등 PUT/DELETE
POST without idempotency-key
POST with idempotency-key

안티패턴

  • 모든 에러 재시도: 4xx 도 무한 시도. 사용자 잘못 한 요청을 100번 보냄.
  • jitter 없음: 모든 클라이언트가 같은 시간 재시도 = thundering herd.
  • 무한 재시도: maxAttempts 또는 deadline.
  • POST 재시도 + idempotency-key 없음: 두 번 결제.
  • Retry-After 무시: 서버가 "1분 뒤" 라고 알려줬는데 1초 뒤 재시도 → 영구 차단 가능.
  • 재시도 안에 또 재시도 (HTTP 클라이언트 + 우리 wrapper): N×N 폭발.
  • 로그 안 남김: 재시도가 일어나는지 모름. 메트릭 + 알림.

🤖 LLM 활용 힌트

  • "지수 backoff + full jitter + 멱등 보장 + retryable predicate" 4종 세트.
  • 외부 의존성마다 재시도 정책 명시.

🔗 관련 문서