Claude Code 에러 처리 패턴: 경계에서 실패를 분류하고 복구를 설계하기
Claude Code로 API 검증, 외부 API 실패, 배치 실패를 분류하고 TypeScript로 복구 가능한 에러 처리를 설계합니다.
에러 처리는 try-catch를 많이 쓰는 일이 아닙니다. Claude Code에게 “이 에러를 고쳐줘”라고만 요청하면, 모든 실패가 500으로 뭉개지거나 로그에 검색하기 어려운 문자열만 남는 경우가 많습니다. 운영에서 중요한 것은 실패를 어디에서 분류하고, 어떻게 복구할지 명시하는 것입니다.
이 글의 핵심 패턴은 초보자 관점에서 이렇게 설명할 수 있습니다. “경계에서 실패를 분류하고 복구 방법을 명시하는 설계”입니다. 경계란 API 요청, 외부 결제 서비스, CRM, 메일 발송, 정기 작업, CSV 가져오기처럼 애플리케이션 내부와 외부가 만나는 지점입니다. 경계가 정리되면 Claude Code도 더 안전하게 수정할 수 있습니다. 사용자가 고칠 수 있는 입력 오류는 400, 일시적인 외부 서비스 장애는 재시도, 배치 실패는 재실행 가능한 기록으로 남기는 식입니다.
이름보다 복구 방법을 먼저 정한다
AppError라는 이름이 좋은지, DomainError가 좋은지보다 먼저 결정해야 하는 것은 다음 행동입니다.
| 경계 | 예시 | 응답 | 복구 방법 |
|---|---|---|---|
| API 입력 검증 | 잘못된 이메일, 범위를 벗어난 값, 깨진 JSON | 400 | 사용자가 입력을 수정 |
| 외부 API | 결제, CRM, 메일 공급자 장애 | 502 또는 503 | 재시도, 일시 중단, 임시 실패 표시 |
| Job/Batch | 야간 리포트, CSV 가져오기, 알림 발송 실패 | 내부 실패 기록 | 안전한 재실행, 실패 큐, 알림 |
문자열 message만으로는 부족합니다. 사람이 읽을 수는 있지만 프로그램이 분기하기 어렵습니다. kind, code, retryable, status 같은 기계가 읽을 수 있는 값을 남기면 Claude Code가 동작 기준으로 리뷰할 수 있습니다.
복사해서 실행할 수 있는 TypeScript 예제
다음 코드는 Node.js 18 이상에서 실행할 수 있습니다. 실제 외부 API를 호출하지 않고, 테스트 가능한 fetcher를 주입해 실패를 재현합니다.
npm install -D tsx typescript
npx tsx error-patterns-demo.ts
type Kind = "validation" | "external" | "job";
type AppError = { kind: Kind; code: string; message: string; retryable: boolean; status: number; detail?: unknown };
type Result<T> = { ok: true; value: T } | { ok: false; error: AppError };
const ok = <T>(value: T): Result<T> => ({ ok: true, value });
const fail = <T>(error: AppError): Result<T> => ({ ok: false, error });
function parseUser(body: unknown): Result<{ email: string; age: number }> {
if (typeof body !== "object" || body === null) {
return fail({ kind: "validation", code: "BODY_REQUIRED", message: "body must be an object", retryable: false, status: 400 });
}
const data = body as Record<string, unknown>;
if (typeof data.email !== "string" || !data.email.includes("@")) {
return fail({ kind: "validation", code: "EMAIL_INVALID", message: "email is invalid", retryable: false, status: 400 });
}
if (typeof data.age !== "number" || data.age < 13) {
return fail({ kind: "validation", code: "AGE_INVALID", message: "age must be 13 or greater", retryable: false, status: 400 });
}
return ok({ email: data.email, age: data.age });
}
async function callPartner(fetcher: () => Promise<Response>): Promise<Result<{ id: string }>> {
try {
const response = await fetcher();
if (!response.ok) {
return fail({ kind: "external", code: "PARTNER_HTTP", message: `partner returned ${response.status}`, retryable: response.status >= 500, status: 503 });
}
const json = (await response.json()) as { id?: unknown };
if (typeof json.id !== "string") {
return fail({ kind: "external", code: "PARTNER_PAYLOAD", message: "partner payload is invalid", retryable: false, status: 502, detail: json });
}
return ok({ id: json.id });
} catch (error) {
return fail({ kind: "external", code: "PARTNER_UNREACHABLE", message: "partner is unreachable", retryable: true, status: 503, detail: error });
}
}
async function runJob<T>(name: string, work: () => Promise<T>, retries = 2): Promise<Result<T>> {
for (let attempt = 1; attempt <= retries + 1; attempt += 1) {
try {
return ok(await work());
} catch (error) {
if (attempt <= retries) continue;
return fail({ kind: "job", code: "JOB_FAILED", message: `${name} failed`, retryable: true, status: 500, detail: { attempt, error } });
}
}
return fail({ kind: "job", code: "JOB_FAILED", message: `${name} failed`, retryable: true, status: 500 });
}
console.log(parseUser({ email: "bad", age: 10 }));
console.log(await callPartner(async () => new Response("down", { status: 503 })));
console.log(await runJob("daily-report", async () => ({ exportedRows: 42 })));
사례1: API 입력 검증
API 입력 검증 실패는 대부분 사용자가 고칠 수 있는 실패입니다. 이메일 형식이 틀렸거나, 나이가 허용 범위보다 작거나, JSON 구조가 잘못된 경우를 서버 내부 장애처럼 보이면 안 됩니다. 예제의 parseUser는 비즈니스 로직에 들어가기 전에 validation으로 분류하고, status: 400, retryable: false를 반환합니다.
이렇게 하면 프런트엔드는 필드를 표시할 수 있고, 백엔드는 EMAIL_INVALID로 로그를 검색할 수 있으며, Claude Code는 정상 경로뿐 아니라 실패 경로 테스트를 추가하기 쉬워집니다. API 테스트를 더 체계화하려면 Claude Code API 테스트 자동화와 테스트 전략 가이드를 함께 보는 것이 좋습니다.
사례2: 외부 API 실패
외부 서비스는 우리 코드가 맞아도 실패합니다. 결제, 메일, CRM, 스프레드시트, 분석 도구는 모두 타임아웃, 레이트 리밋, 잘못된 응답을 만들 수 있습니다. 여기서 중요한 것은 재시도 가능한 실패와 재시도하면 안 되는 실패를 분리하는 것입니다.
예제의 callPartner는 5xx를 retryable: true로 보고, 응답 구조가 깨진 경우는 retryable: false로 둡니다. 구조가 틀린 응답을 계속 재시도하면 로그와 큐만 더러워집니다. HTTP 응답 판정은 공식 MDN Response.ok를 확인하세요. Express를 쓴다면 Express error handling도 함께 확인해야 합니다.
사례3: Job/Batch 실패
잡이나 배치는 화면 API보다 실패가 늦게 보입니다. 야간 리포트, CSV 가져오기, 청구서 생성, 알림 발송은 사용자가 기다리지 않는 대신 실패가 방치되기 쉽습니다. runJob은 작업 이름, 최종 시도 횟수, 재시도 가능 여부를 남깁니다. 실제 운영에서는 이 정보를 구조화 로그, 실패 큐, 관리자 화면, 알림으로 보내야 합니다.
가장 중요한 질문은 “다시 실행해도 안전한가”입니다. CSV를 다시 가져왔을 때 중복 행이 생기거나, 결제 잡을 재실행했을 때 중복 청구가 발생한다면 리트라이는 장애 대응이 아니라 사고 확대입니다. Claude Code에게도 문법보다 재실행 안전성을 리뷰하게 해야 합니다.
자주 생기는 함정
첫 번째 함정은 catch { return null; }로 실패를 없애는 것입니다. 증거가 사라지면 Claude Code도 사람도 원인을 추적할 수 없습니다. 두 번째는 SQL 이름, 환경 변수, 스택 트레이스, 토큰 조각을 사용자 응답에 넣는 것입니다. 보안 관점은 Claude Code 보안 감사 자동화를 참고하세요. 세 번째는 모든 실패를 재시도하는 것입니다. 검증 오류와 잘못된 응답 구조는 시간이 지나도 고쳐지지 않습니다.
Claude Code 리뷰 프롬프트
구현 후에는 다음 프롬프트로 리뷰시키면 좋습니다.
이 PR의 에러 처리를 리뷰해 주세요.
관점:
1. API 입력, 외부 API, Job/Batch 경계에서 실패를 분류했는가
2. 사용자가 고칠 수 있는 실패를 500으로 반환하지 않는가
3. retryable과 non-retryable을 분리했는가
4. 응답에 stack trace, SQL, 비밀값, 내부 경로가 노출되지 않는가
5. 로그에 code, kind, attempt, cause가 남는가
6. 실패 경로 테스트가 3개 이상 있는가
부족하면 최소 수정안과 테스트를 제안해 주세요.
TypeScript의 판별 유니온과 타입 좁히기는 공식 TypeScript Narrowing을 참고하세요. 실패 경로를 표준 테스트로 고정하려면 Node.js test runner가 유용합니다. Claude Code 설정과 프로젝트 메모리는 Claude Code overview에서 확인할 수 있습니다. 테스트 우선으로 진행한다면 Claude Code TDD 실천 가이드와 디버깅 테크닉이 이어집니다.
CTA와 Masa의 실행 결과
에러 처리는 팀 교육에 잘 맞는 주제입니다. API, 로그, 보안, 테스트, 운영이 한 번에 연결되기 때문입니다. 혼자 시작한다면 무료 치트시트를 받아 보세요. 프롬프트와 리뷰 체크리스트, 템플릿이 필요하면 상품 목록이 빠릅니다. 기존 리포지토리에 Claude Code 기준의 에러 처리 규칙을 넣고 싶다면 Claude Code 교육 및 도입 상담에서 API, 외부 연동, 배치 경계를 함께 정리할 수 있습니다.
Masa의 손元 환경에서는 입력 검증, 외부 API, 배치 실패를 비슷한 AppError 형태로 맞춘 뒤 Claude Code 지시가 짧아졌습니다. 예전에는 “이 에러를 잘 고쳐줘”라고 했지만, 지금은 “validation은 400, external은 retryable로 판단, job은 attempt를 남김”처럼 말할 수 있습니다. 리뷰도 응답, 로그, 테스트 세 곳으로 좁혀졌습니다. 완벽한 구조는 아니지만 익명의 500이 많은 코드보다 훨씬 운영하기 쉬웠습니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Claude Code Permission Receipt Pattern: 권한, 증거, 롤백을 남기는 운영
Claude Code 작업마다 허용 범위, 승인 경계, 검증 명령, 롤백 메모, Gumroad와 상담 CTA 확인을 남기는 permission receipt 패턴입니다.
Claude Code/Codex 안전 Agent Harness 설계: 권한, 검증, 롤백
Claude Code와 Codex를 안전하게 운영하기 위한 Agent Harness를 권한 정책, 실행 계획, 검증, 복구 계층으로 설계합니다.
Claude Code 서브에이전트 실전 가이드: 기사와 코드 작업을 안전하게 위임하기
Claude Code 서브에이전트로 기사와 코드 작업을 안전하게 나누는 방법. 위임 규칙, 프롬프트, 실패 사례를 정리합니다.