15장 — WITH-CONDITIONS 깊이
CHAPTER 15
WITH-CONDITIONS 깊이
예외·복귀 흐름의 3층 구조
📑 이 챕터에서 다룰 내용

새 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% — 이게 정상입니다.

15-1 예외 종류 3가지 🔗

실전 예외는 다음 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에서 다룹니다).

15-2 복귀 흐름 3가지 — try/catch·circuit breaker·retry 🔗

예외가 발생하면 "어떻게 복귀하는가"의 흐름. 도구 3개로 분리합니다.

도구 1: try/catch — 즉시 fallback

가장 단순한 도구. 예외 발생 시 "즉시 다른 흐름"으로 전환합니다.

💻 LLM 분류 실패 시 NULL 처리
// 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" — 외부 시스템 회복 대기합니다.

💻 CircuitBreaker 구현
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초 간격으로 자동 재시도합니다.

💻 retryWithBackoff 구현
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)장기 안정성
15-3 5개 조건 이상 → 별도 함수로 분리 🔗

조건부 분기가 한 함수 안에 5개 이상이면 "코드 폭발"의 신호. 별도 함수·모듈로 분리합니다.

분리 전 — 코드 폭발 패턴

💻 ❌ 한 함수에 9개 조건 — 6개월 후 동업자 펼치면 못 읽음
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 규칙

📘 분리 의식 3 규칙

[규칙 1] 한 함수의 조건부 분기 5개 이상 → 분리

[규칙 2] 함수 이름이 "왜 이 분기"의 답이 되도록

[규칙 3] 분리 시 LogOnTable에 "왜 분리" 1줄 트레이스

15-4 LogOnTable 활용 — 예외 발생 → SPEC v2/v3 환류 🔗

예외가 발생할 때마다 LogOnTable에 "이 예외를 어떻게 다뤘는가"를 1~3줄 트레이스. 6개월 누적이 SPEC v2/v3 진화의 핵심 입력값이 됩니다.

예외 LogOnTable 양식

💻 BUILD.md 예외 트레이스 양식
[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 알림 복귀 흐름
[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쪽으로 박힙니다.

15-5 예외 처리 함정 4개 🔗

예외 처리에는 함정이 있습니다.

함정 1: 모든 예외를 catch → 진짜 오류가 묻힘

💻 ❌ 함정 — 모든 예외를 catch 후 무시
try {
  await callApi();
} catch {
  // 아무것도 안 함
}

해결: catch에서 "무엇을 어떻게 다뤘는지" 명시. 무시하려면 명시적 "무시 사유" 1줄.

함정 2: retry 무한 → 외부 시스템 부하

💻 ❌ 함정 — 무한 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줄. "왜" 보존.

15-6 WITH-CONDITIONS 75분 정정렬에 예외·복귀 흐름 박기 🔗

새 14장의 WITH-CONDITIONS 75분 흐름에 "예외·복귀"를 결합하는 방법입니다.

75분 흐름 + 예외·복귀 추가

1
Step 1 - 15분 — 조건부 분기 식별 + 예외 종류 3 분류
기존 15분 분기 식별에 예외 종류 3가지 (사용자 행동·외부 API·시간) 분류를 추가합니다.
2
Step 2 - 20분 — 분기별 흐름 명시 + 복귀 흐름 결합
분기별 흐름 명시에 복귀 흐름 (try/catch·circuit·retry) 결합. 어느 도구를 어디에 쓸지 결정합니다.
3
Step 3 - 15분 — 5개 이상 분기 → 별도 함수 분리
한 함수에 5개 이상 조건이 있으면 의식적으로 분리. 함수 이름이 "왜 이 분기"의 답이 되도록.
4
Step 4 - 15분 — 단위 테스트 + 예외 케이스 5개 추가
정상 케이스 5개 + 예외 케이스 5개 = 총 10개 테스트. 예외 케이스가 없으면 테스트 의미가 절반입니다.
5
Step 5 - 10분 — LogOnTable 트레이스
정정렬 + 예외 처리 결정 1~3줄. "왜 이 복귀 흐름" 1줄 보존.
💡 75분 → 95~105분의 가치

기존 75분 흐름이 +20~30분 정도로 늘어나지만, 6개월 후 "왜 이 예외 처리" 답이 보존되어 운영 매뉴얼 가치가 폭증합니다.

📌 새 15장 정리
  • 핵심 한 줄: 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 진화
🤖
1탄 학습 도우미
질문하기 OK
안녕하세요! WITH-CONDITIONS 깊이에 대해 무엇이든 물어보세요. 본문에서 찾아 답변해드릴게요. 👇