에러 핸들링 & Retry 전략
Book Print API 호출 시 발생하는 에러를 안정적으로 처리하기 위한 운영 패턴을 안내합니다. 재시도 가능 여부 분류, Backoff 전략, Idempotency-Key 결합, Timeout·Stop Conditions 등 복원력 있는 클라이언트 작성에 필요한 가이드입니다.
errorCode 카탈로그·응답 shape·케이스 예시는 에러 코드 & 트러블슈팅을, Idempotency-Key 동작 자체는 멱등성을, Rate Limit 정책은 Rate Limiting을 참고하세요. 본 페이지는 이 메커니즘들을 운영 시나리오로 엮은 종합 가이드입니다.재시도 가능 vs 재시도 불가
errorCode를 기준으로 재시도 가능 여부를 분류합니다. 재시도 가능 에러는 transient(일시적)이며, 재시도 불가 에러는 요청·환경·코드 변경 없이 반복해도 같은 결과가 반환됩니다.
| HTTP | errorCode | 재시도 | 권장 동작 |
|---|---|---|---|
400 | ERR_VALIDATION_FAILEDERR_MALFORMED_REQUESTERR_INSUFFICIENT_PAGESERR_PAGECOUNT_INVALIDERR_FINALIZE_PREREQ_UNMETERR_CREATION_TYPE_UNSUPPORTED | X | 요청 본문 수정 후 호출. fieldErrors 파싱하여 사용자 안내 |
401 | ERR_UNAUTHORIZED | X | API Key 확인 (만료·환경 불일치 등). 동일 호출 재시도해도 같은 결과 |
402 | ERR_INSUFFICIENT_CREDIT | 조건부 | 충전 후 재시도 가능. 자동 충전은 권장 안 함 (의도하지 않은 결제 방지) |
403 | ERR_FORBIDDEN / ERR_ENV_MISMATCH | X | 권한·환경 설정 수정. ERR_ENV_MISMATCH는 호출 도메인 또는 대상 리소스 환경 확인 |
404 | ERR_NOT_FOUND / ERR_PDF_NOT_UPLOADED | X | 리소스 UID 확인. ERR_PDF_NOT_UPLOADED는 PDF 업로드 후 호출 |
409 | ERR_CONFLICT (리소스 상태 충돌) | X | 이미 등록된 리소스에 POST 재호출 등. 같은 요청을 재시도해도 같은 결과 — 요청 흐름 수정 필요 |
409 (Idempotency 락 타임아웃 — 응답에 errorCode 필드 부재) | O | 같은 Idempotency-Key로 동시 호출 시 락 대기 타임아웃. 일정 간격 후 같은 키로 재시도하면 첫 응답 재사용. 자세한 분기 안내는 아래 alert 참조 | |
ERR_PDF_NOT_GENERATED | O | POST /finalization 호출 후 재시도 | |
ERR_PDF_PENDING | O | 일정 간격(예: 2초~10초) 후 재시도. 무한 폴링 금지 | |
422 | ERR_IDEMPOTENCY_KEY_MISMATCH | X | 본문이 바뀌어야 한다면 새 Idempotency-Key 발급 후 재시도 |
ERR_PDF_GENERATION_FAILED | X | 클라이언트 자체 해결 불가 — 고객지원팀 문의 | |
429 | ERR_TOO_MANY_REQUESTS | O | Retry-After 헤더 값(초)만큼 대기 후 재시도. Backoff 결합 권장 |
500 | ERR_INTERNAL_ERROR | O | 서버 측 일시적 문제. Backoff 적용 재시도. 지속 시 고객지원팀 문의 |
ERR_PDF_FILE_MISSING | X | 서버 결함 — 고객지원팀 문의 | |
501 | ERR_SANDBOX_UNSUPPORTED | X | 의도적 Sandbox 차단. 자동 재시도 화이트리스트 제외 필수 |
errorCode로 분기: 같은 HTTP 상태라도 Specific errorCode에 따라 재시도 가능 여부가 다릅니다 (예: 500의 ERR_INTERNAL_ERROR는 재시도 O, ERR_PDF_FILE_MISSING은 재시도 X). 카탈로그 전체는 에러 코드 & 트러블슈팅을 참조하세요.ERR_CONFLICT(리소스 상태 충돌)는 6필드 응답이지만, Idempotency 락 타임아웃 응답은 errorCode 필드가 부재합니다 (현재 서버 동작). 두 시나리오를 구분하려면 응답 본문에 errorCode가 있는지 먼저 확인하고, 없으면 락 타임아웃으로 분기해 같은 Idempotency-Key로 재시도하세요. 6필드 통일은 향후 서버 개선 예정.Exponential Backoff + Jitter
transient 에러(409·429·500 등) 재시도 시 대기 시간을 지수적으로 증가시키고 약간의 무작위(jitter)를 추가합니다. jitter는 여러 클라이언트가 동시에 재시도해 서버에 부하를 주는 thundering herd를 방지합니다.
- 초기 대기 시간: 1초 권장
- 증가율: 2배씩 (1s → 2s → 4s → 8s ...)
- 최대 대기 시간: 30초 권장 (운영 SLA에 맞게 조정)
- 최대 시도 횟수: 3~5회 권장
- jitter: 계산된 대기 시간의 ±20% 무작위 가산
429 응답의 경우 서버가 Retry-After 헤더(초)를 동봉합니다. 이 값을 우선 사용하고, 그 이상 대기가 필요하면 Backoff를 결합합니다.
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function computeBackoff(attempt, retryAfterSec) {
// 429 응답에 Retry-After 헤더 있으면 우선 사용
if (retryAfterSec) {
return retryAfterSec * 1000;
}
// 1초 → 2초 → 4초 → 8초 ... 최대 30초
const base = Math.min(1000 * Math.pow(2, attempt), 30000);
// jitter ±20%
const jitter = base * 0.2 * (Math.random() * 2 - 1);
return base + jitter;
}
// errorCode 기반으로 재시도 가능 여부 판정
async function isRetryable(response) {
if (response.status === 429) return true;
if (response.status === 500) {
// 500은 ERR_INTERNAL_ERROR(재시도 O) vs ERR_PDF_FILE_MISSING(재시도 X) 분기
const body = await response.clone().json().catch(() => null);
return body?.errorCode !== 'ERR_PDF_FILE_MISSING';
}
if (response.status === 409) {
const body = await response.clone().json().catch(() => null);
// 락 타임아웃 (errorCode 필드 부재) → 재시도 O
if (!body?.errorCode) return true;
// ERR_PDF_NOT_GENERATED / ERR_PDF_PENDING → 재시도 O
if (body.errorCode === 'ERR_PDF_NOT_GENERATED'
|| body.errorCode === 'ERR_PDF_PENDING') return true;
// ERR_CONFLICT (리소스 상태 충돌) → 재시도 X
return false;
}
return false;
}
async function withRetry(fetchFn, maxAttempts = 4) {
let lastResponse;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
lastResponse = await fetchFn();
if (!(await isRetryable(lastResponse))) {
return lastResponse;
}
if (attempt === maxAttempts - 1) {
return lastResponse; // 마지막 시도, 그대로 반환
}
const retryAfter = lastResponse.headers.get('Retry-After');
const waitMs = computeBackoff(attempt, retryAfter ? parseInt(retryAfter, 10) : null);
await sleep(waitMs);
}
return lastResponse; // 도달하지 않지만 컴파일러 경고 회피
}Idempotency-Key + Retry 결합
네트워크 오류 또는 transient 에러로 인한 재시도 시 같은 Idempotency-Key를 재사용하면 서버가 첫 응답을 캐시해 중복 처리를 방지합니다. POST /orders·POST /orders/{orderUid}/cancel·POST /orders/{orderUid}/items/{itemUid}/cancel 등 부수효과 있는 호출에는 반드시 활용하세요. 적용 엔드포인트 전체 목록은 멱등성 (Idempotency)을 참고하세요.
- 같은 키 + 같은 본문: 첫 응답이 그대로 재사용됨 (멱등 동작)
- 같은 키 + 다른 본문:
422 ERR_IDEMPOTENCY_KEY_MISMATCH반환 → 본문이 바뀌어야 한다면 새 키 발급 - 키 유효 기간: 첫 응답 캐시 보유 60초(
LockExpiry). 다른 클라이언트가 같은 키로 동시 호출하면 락 획득 30초 대기(LockTimeout) — 타임아웃 시 409 응답(위 alert 참조)
상세 동작과 적용 엔드포인트는 멱등성 (Idempotency)을 참조하세요.
// crypto.randomUUID()는 Node.js 14.17+ / 브라우저 표준 API. 별도 라이브러리 불요.
async function createOrderWithRetry(orderData) {
const idempotencyKey = crypto.randomUUID();
const baseUrl = 'https://api-sandbox.sweetbook.com';
const headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Idempotency-Key': idempotencyKey,
'Content-Type': 'application/json; charset=utf-8',
};
return await withRetry(async () => {
return fetch(`${baseUrl}/v1/orders`, {
method: 'POST',
headers,
body: JSON.stringify(orderData),
});
});
}Rate Limit (429) 대응
429 ERR_TOO_MANY_REQUESTS 응답에는 Retry-After 헤더가 동봉됩니다. 헤더 값(초)만큼 대기한 뒤 재시도하세요.
Retry-After헤더 값을 우선 사용- 헤더 값 이상 대기가 필요하면 Backoff 적용
- 대량 호출 패턴(예: 일괄 책 생성)에서는 요청 간 간격을 두는 throttle을 클라이언트 측에 구현 권장
요청 제한 정책(분당 제한값·버킷 단위 등)은 Rate Limiting을 참조하세요.
Timeout & Stop Conditions
무한 대기·무한 재시도를 방지하려면 다음 stop conditions를 클라이언트에 적용하세요.
- 한 시도당 timeout: 단일 HTTP 호출이 일정 시간 안에 응답 안 하면 끊고 재시도. 운영 SLA에 맞게 결정 (일반적으로 30초 전후)
- 전체 deadline: 재시도 포함 전체 작업의 최대 시간. 사용자 응답 흐름에서는 짧게(예: 5분), 백그라운드 작업은 길게 설정 가능
- 최대 시도 횟수: 3~5회 권장. 그 이상은 transient 에러가 아닐 가능성이 높으므로 클라이언트에서 알람·로그를 통해 확인
자동 재시도 화이트리스트 제외
아래 에러는 클라이언트의 자동 재시도 로직에서 의도적으로 제외해야 합니다. 재시도해도 같은 결과가 반환되거나 의도하지 않은 부수효과가 발생합니다.
| 코드 | 이유 | 대응 |
|---|---|---|
501 ERR_SANDBOX_UNSUPPORTED | 의도적 Sandbox 차단 (Live 전용 엔드포인트) | 호출 환경 확인. 환경 매트릭스 참조 |
422 ERR_IDEMPOTENCY_KEY_MISMATCH | 같은 Idempotency-Key에 다른 본문 — 자동 재시도해도 본문 동일하지 않은 한 같은 결과 | 본문이 바뀌어야 한다면 새 키 발급. 같은 본문이라면 첫 응답 사용 |
422 ERR_PDF_GENERATION_FAILED | 서버 측 PDF 생성 실패 — 클라이언트 자체 해결 불가 | 고객지원팀 문의 |
500 ERR_PDF_FILE_MISSING | 서버 결함 (생성 완료 상태인데 파일 부재) | 고객지원팀 문의 |
종합 코드 패턴 — 견적 → 주문 (Retry + Backoff + Idempotency)
앞서 다룬 메커니즘들을 결합한 운영 패턴 예시입니다. POST /orders/estimate로 잔액 검증 후 POST /orders로 주문 생성하며, transient 에러는 자동 재시도합니다.
- 견적은 read-only(
order:read)이므로 멱등 안전 — Idempotency-Key 불필요 - 주문 생성은 부수효과 있음 — Idempotency-Key 필수, 재시도 시 같은 키 재사용
- 402(잔액 부족)는 자동 재시도 X — 클라이언트가 충전 안내. 충전금 운영 절차는 충전금 운영 절차 참조
// withRetry / sleep / computeBackoff / isRetryable 헬퍼는 위 "Exponential Backoff + Jitter" 섹션 코드 사용.
async function placeOrder(orderData) {
const baseUrl = 'https://api-sandbox.sweetbook.com';
const headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json; charset=utf-8',
};
// 1. 견적 (멱등 안전, Idempotency-Key 불요. transient 에러만 재시도)
const estResponse = await withRetry(() =>
fetch(`${baseUrl}/v1/orders/estimate`, {
method: 'POST', headers, body: JSON.stringify(orderData),
})
);
const est = await estResponse.json();
if (!est.data?.creditSufficient) {
// 402 사전 차단 (자동 충전 금지)
throw new Error(
`충전금 부족. 필요: ${est.data.paidCreditAmount}, 잔액: ${est.data.creditBalance}`
);
}
// 2. 주문 생성 (Idempotency-Key 필수, transient 에러는 같은 키로 재시도)
const idempotencyKey = crypto.randomUUID();
const orderResponse = await withRetry(() =>
fetch(`${baseUrl}/v1/orders`, {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': idempotencyKey },
body: JSON.stringify(orderData),
})
);
// 3. 402 재확인 (견적 통과 후 동시 차감 케이스)
if (orderResponse.status === 402) {
const body = await orderResponse.json();
throw new Error(
`충전금 부족 (동시성). 필요: ${body.data.required}, 잔액: ${body.data.balance}`
);
}
return orderResponse.json();
}