📑 이 챕터에서 다룰 내용
새 14장에서 WITH-CONDITIONS 흐름을 봤습니다 (조건부 분기 75분 만에 정정렬). 그러나 실전에서는 "행복 경로 (happy path)"만으로 운영되는 시스템이 없습니다. 사용자 행동·외부 API 응답·시간 — 어디서든 예외가 발생합니다.
이 장은 "WITH-CONDITIONS의 깊이"입니다. 예외가 발생할 때 어떻게 복귀하는가, 코드가 폭발하지 않게 어떻게 분리하는가, LogOnTable에 어떻게 환류하는가.
사전 지식: 새 14장 WITH-CONDITIONS 흐름 / 새 9장 BUILD.md + E4 LogOnTable
이 장의 목적: 예외 종류 3가지 + 복귀 흐름 3가지 + 5개 조건 이상 분리 의식 + LogOnTable 환류 흐름
완료 후 결과물: 예외·복귀 흐름이 SPEC v2/v3에 환류 + 코드 폭발 없는 조건부 분기
신입 개발자가 처음 시스템을 만들면 행복 경로만 코드에 박습니다. "사용자가 정상 입력하고, API가 정상 응답하고, 시간이 정상 흐를 때"의 코드. 그러나 실전에서는 5번에 1번이 비정상입니다.
행복 경로 75% + 예외 25%가 아니라, 행복 경로 코드 30% + 예외 처리 코드 70% — 이게 정상입니다.
실전 예외는 다음 3가지 종류로 분류됩니다.
종류 1: 사용자 행동 예외
사용자가 의도하거나 의도하지 않게 "비정상 입력"을 합니다.
| 사례 | 발생 빈도 | 영향 |
|---|---|---|
| 양식 입력 중 브라우저 새로고침 | 매일 | 작업 손실 |
| 결제 진행 중 뒤로가기 | 매일 | 결제 중복 위험 |
| 로그인 후 30분 방치 | 매시간 | 세션 만료 |
| 이미지 업로드 중 네트워크 끊김 | 매일 | 부분 업로드 |
| 같은 버튼 더블 클릭 | 매시간 | 중복 요청 |
종류 2: 외부 API 응답 예외
외부 시스템이 "정상 응답을 안 함" 또는 "비정상 응답을 함".
| 사례 | 발생 빈도 | 영향 |
|---|---|---|
| Stripe 결제 시점 timeout | 매주 1~2회 | 결제 상태 불명 |
| Supabase RLS 정책 거부 | 매일 | 데이터 못 가져옴 |
| 정부 API rate limit | 매주 | 데이터 시딩 X |
| LLM API 비정상 JSON 출력 | 매일 | 분류 실패 |
| 외부 webhook 5xx 응답 | 매주 | 알림 누락 |
종류 3: 시간 예외
시간 흐름 자체가 만드는 예외입니다.
| 사례 | 발생 빈도 | 영향 |
|---|---|---|
| 마감일 지난 콘텐츠가 노출 | 매일 | 사용자 혼란 |
| Cron 작업이 두 번 실행 | 매주 | 중복 처리 |
| 시간대 전환 (DST) | 연 2회 | 알림 시간 오류 |
| 새해 전후 (12/31 → 1/1) | 연 1회 | 통계 갱신 X |
| timestamp가 비정상 (미래·과거) | 매일 | 정렬 깨짐 |
- 사용자 행동 예외: 운영 후 사용자 항의로 발견
- 외부 API 예외: 모니터링 (Sentry)으로 발견
- 시간 예외: 6개월 후 데이터 점검 시 발견
발견 흐름이 다르므로 대응 도구도 다릅니다 (15-3에서 다룹니다).
예외가 발생하면 "어떻게 복귀하는가"의 흐름. 도구 3개로 분리합니다.
도구 1: try/catch — 즉시 fallback
가장 단순한 도구. 예외 발생 시 "즉시 다른 흐름"으로 전환합니다.
// LLM 분류 실패 시 NULL 처리
async function classifyBenefit(rawText: string): Promise<Classification | null> {
try {
const result = await callLLM(rawText);
if (result.confidence < 0.7) return null; // 임계값 미만도 NULL
return result;
} catch (e) {
log("LLM 분류 실패", { rawText, error: e.message });
return null; // fallback: NULL → 검수 큐로
}
}적용 영역: 단발 호출 (LLM·DB 단순 query). 즉시 fallback 가능한 경우.
도구 2: circuit breaker — 외부 시스템 보호
외부 시스템이 5번 연속 실패하면 "60초 동안 호출 X" — 외부 시스템 회복 대기합니다.
class CircuitBreaker {
private failures = 0;
private openUntil: number | null = null;
async call<T>(fn: () => Promise<T>): Promise<T | null> {
if (this.openUntil && Date.now() < this.openUntil) {
return null; // 회로 열림 → 호출 안 함
}
try {
const result = await fn();
this.failures = 0;
return result;
} catch (e) {
this.failures++;
if (this.failures >= 5) {
this.openUntil = Date.now() + 60_000; // 60초 차단
log("Circuit breaker OPEN", { failures: this.failures });
}
throw e;
}
}
}적용 영역: 외부 API 의존도 높은 흐름 (정부 API·Stripe·webhook). 시스템 보호.
도구 3: retry with backoff — 일시 오류 자동 재시도
외부 시스템 "일시 오류" 시 1초·3초·9초 간격으로 자동 재시도합니다.
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxAttempts = 3
): Promise<T> {
let lastError: Error;
for (let i = 0; i < maxAttempts; i++) {
try {
return await fn();
} catch (e) {
lastError = e as Error;
if (i < maxAttempts - 1) {
await sleep(1000 * Math.pow(3, i)); // 1s · 3s · 9s
}
}
}
throw lastError!;
}적용 영역: 일시 오류가 잦은 외부 API (rate limit·timeout). 5초 이내 회복 가능한 경우.
3 도구 선택 기준
| 예외 성격 | 권장 도구 | 근거 |
|---|---|---|
| 단발·즉시 fallback 가능 | try/catch | 가장 가벼움 |
| 외부 시스템 의존도 높음 | circuit breaker | 시스템 보호 |
| 일시 오류 잦음 | retry with backoff | 자동 회복 |
| 셋 다 해당 | 3개 결합 (외측→내측: circuit→retry→try/catch) | 장기 안정성 |
조건부 분기가 한 함수 안에 5개 이상이면 "코드 폭발"의 신호. 별도 함수·모듈로 분리합니다.
분리 전 — 코드 폭발 패턴
async function processOrder(order: Order) {
if (order.status === "pending") {
if (order.user.tier === "premium") {
if (order.total > 100) {
if (order.createdAt > Date.now() - 24 * 3600_000) {
if (order.items.length > 5) {
// ...
} else {
// ...
}
} else if (order.user.country === "KR") {
// ...
}
}
} else if (order.user.tier === "free") {
// ...
}
} else if (order.status === "paid") {
// ...
}
}분리 후 — 의식적 분리
async function processOrder(order: Order) {
if (order.status !== "pending") return processPaidOrder(order);
if (order.user.tier === "premium") return processPremiumOrder(order);
if (order.user.tier === "free") return processFreeOrder(order);
return processDefaultOrder(order);
}
async function processPremiumOrder(order: Order) {
if (order.total > 100 && isWithin24Hours(order)) {
return order.items.length > 5
? processBulkPremiumOrder(order)
: processStandardPremiumOrder(order);
}
return processStandardPremiumOrder(order);
}분리 의식 3 규칙
[규칙 1] 한 함수의 조건부 분기 5개 이상 → 분리
[규칙 2] 함수 이름이 "왜 이 분기"의 답이 되도록
[규칙 3] 분리 시 LogOnTable에 "왜 분리" 1줄 트레이스
예외가 발생할 때마다 LogOnTable에 "이 예외를 어떻게 다뤘는가"를 1~3줄 트레이스. 6개월 누적이 SPEC v2/v3 진화의 핵심 입력값이 됩니다.
예외 LogOnTable 양식
[BUILD.md Day N — 예외 트레이스] ## Day 17 — LLM 분류 실패 5건 발생 [발생 시점] 14:32 KST [예외 종류] LLM JSON 비정상 출력 (confidence 0.4) [복귀 흐름] try/catch → NULL 처리 → 검수 큐 적재 [근본 원인] 정부 API 응답에 영문 혼용 (한국어 분류 프롬프트) [다음 SPEC 입력값] SPEC.md §LLM 분류에 "영문 혼용 처리" 추가 필요
이 트레이스가 6개월 누적되면 SPEC v2 → v3 진화 시 자동으로 "보강 영역"이 보입니다.
Junho 적용 사례 — TSV 자동 발행 실패 시 복귀 흐름
[Day 25 — Sentry 알림] - 페르소나 자동 일관성 테스트 1건 fail - 5확장 E5 [4] 자동 정지 트리거 작동 - cron/publish.ts 즉시 정지 [복귀 흐름] 1. Sentry 알림 → 운영자 디스코드 즉시 통지 2. 운영자 5분 내 점검 — 페르소나 출력 환각 발견 3. SSOT 인터페이스 (lib/match_facts.ts) 검토 → 영향 X 4. 페르소나 system prompt 수정 (cache_control 영향 인지) 5. 일관성 테스트 재실행 → 통과 6. cron/publish.ts 재시작 [BUILD.md Day 25 트레이스] - 사건: 페르소나 일관성 fail 1건 - 원인: 페르소나 system prompt에 통계 추정 표현 잠입 - 복귀: 5분 (Sentry → 디스코드 → 즉시 점검) - SPEC 입력값: SPEC §페르소나 시스템 프롬프트 작성 가이드 보강 필요
이 트레이스가 시리즈 약 15개월 누적 → SPEC v3 진화 시 "페르소나 시스템 프롬프트 작성 가이드"가 1쪽으로 박힙니다.
예외 처리에는 함정이 있습니다.
함정 1: 모든 예외를 catch → 진짜 오류가 묻힘
try {
await callApi();
} catch {
// 아무것도 안 함
}해결: catch에서 "무엇을 어떻게 다뤘는지" 명시. 무시하려면 명시적 "무시 사유" 1줄.
함정 2: retry 무한 → 외부 시스템 부하
while (true) {
try { return await fn(); } catch {}
}해결: maxAttempts 명시 (3~5회). retry 후에도 실패면 fallback.
함정 3: try/catch + circuit breaker 결합 시 충돌
circuit breaker 안에서 try/catch로 예외를 모두 삼키면 circuit이 열리지 않습니다.
해결: try/catch 안에서 "진짜 예외는 다시 throw". circuit breaker가 인지할 수 있게.
함정 4: 예외 로그 X → 6개월 후 답 못 찾음
예외 처리는 됐지만 로그가 없으면 "6개월 후 왜 이게 발생했는지"의 답을 못 찾습니다.
해결: 모든 예외 catch에 log + LogOnTable 트레이스 1줄. "왜" 보존.
새 14장의 WITH-CONDITIONS 75분 흐름에 "예외·복귀"를 결합하는 방법입니다.
75분 흐름 + 예외·복귀 추가
기존 75분 흐름이 +20~30분 정도로 늘어나지만, 6개월 후 "왜 이 예외 처리" 답이 보존되어 운영 매뉴얼 가치가 폭증합니다.
- 핵심 한 줄: WITH-CONDITIONS 흐름 + 예외·복귀 흐름 결합 = 행복 경로 30% + 예외 처리 70%의 실전 코드
- 예외 종류 3개: 사용자 행동 예외 (새로고침·뒤로가기·세션 만료) / 외부 API 응답 예외 (Stripe·Supabase·LLM·webhook) / 시간 예외 (마감일·DST·12/31)
- 복귀 흐름 3 도구: try/catch — 즉시 fallback / circuit breaker — 외부 시스템 보호 / retry with backoff — 일시 오류 자동 회복
- 5개 조건 이상 → 분리 3 규칙: 5개 이상 → 별도 함수 / 함수 이름이 "왜 이 분기"의 답 / LogOnTable에 "왜 분리" 1줄
- 예외 → SPEC 환류: 예외 발생 시 LogOnTable 1~3줄 트레이스 / 6개월 누적 → SPEC v2/v3 진화 자동 입력값
- 함정 4개: 모든 예외 catch → 진짜 오류 묻힘 / retry 무한 → 외부 시스템 부하 / try/catch + circuit 충돌 / 로그 X → 6개월 후 답 X
- 75분 흐름에 예외 추가: 95~105분 (+20~30분). 6개월 운영 매뉴얼 가치 폭증
- 다음 장: 새 16장 — 콘텐츠 SSOT 운영 (장기). 6개월·1년·3년 후 SSOT 진화