refactor(topics): 멀티 에이전트용 지식 재편 — _Common(공통 기본기) + Domain_* 구조
에이전트 8종(대화형/프로그래머 C·S/디자이너/설계자/기획자/QA/PD/PM)에게 [공통 기본 능력 + 롤별 Specialty] 2층으로 지식을 주입하기 위한 재분류. 문서 내용·포맷은 무수정, 폴더 이동만 (6,372개 문서 수 보존 확인). - Topic_Programming → Domain_Programming (내부 구조 보존) - Topic_Graphic → Domain_Design - Topic_Business → Domain_Product - Topic_General → Domain_General - _Common 신설: Math(구 Topic_Math_Specialty), Reasoning(구 General/From_Thinking & Reasoning), Reasoning_Creativity(구 General/From_창의성), Communication(Poetic_Blog_Writing + From_writing) - 타 도메인의 From_* 폴더는 유지 (출처 표기일 뿐, 이미 도메인에 맞게 분류된 문서) - 빈 폴더 정리 (memory/procedures) - 에이전트→폴더 매핑은 workspace의 .astra/agent-knowledge-map.json (9개 에이전트) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
---
|
||||
id: backend-retry-strategy
|
||||
title: Backend 재시도 전략 — 지수 백오프 + Jitter
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [backend, resilience, retry, backoff, vibe-coding]
|
||||
tech_stack: { language: "TypeScript / 모든 언어", applicable_to: ["Backend", "Worker"] }
|
||||
applied_in: []
|
||||
aliases: [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
|
||||
```ts
|
||||
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 와 결합
|
||||
```ts
|
||||
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 헤더 존중
|
||||
```ts
|
||||
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)
|
||||
```ts
|
||||
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종 세트.
|
||||
- 외부 의존성마다 재시도 정책 명시.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Idempotent_Operations]]
|
||||
- [[Backend_Circuit_Breaker]]
|
||||
- [[Backend_Rate_Limiting]]
|
||||
Reference in New Issue
Block a user