Spring

프로덕션 챗봇 서비스의 장애 대응 설계 — 타임아웃, 재시도, Fallback, Feature Toggle 4계층 방어

외부 시스템 의존성이 높은 프로덕션 챗봇의 타임아웃, 재시도, Fallback, Feature Toggle을 통한 4계층 방어 전략

resilience fault-tolerance spring-webflux timeout retry fallback

문제

챗봇 서비스는 복수의 외부 시스템에 의존한다.

  • AI Agent(핵심 응답)
  • OpenAI(STT 변환)
  • EAIP(로그 동기화)
  • Eloqua(CRM 연동)

각 외부 시스템은 독립적으로 장애가 발생할 수 있다. AI Agent 응답은 복잡한 쿼리의 경우 30~60초가 소요된다. 사용자는 반드시 응답을 받아야 하며, 로깅이나 동기화 같은 부수 작업은 메인 채팅 흐름을 방해해서는 안 된다.

4계층 방어 아키텍처

---
config:
  theme: base
  themeVariables:
    primaryColor: "#e0f2fe"
    primaryTextColor: "#0c4a6e"
    primaryBorderColor: "#38bdf8"
    lineColor: "#94a3b8"
    secondaryColor: "#f0fdf4"
    tertiaryColor: "#fef3c7"
    nodeTextColor: "#1e293b"
    edgeLabelBackground: "#ffffff"
    fontSize: "14px"
---
graph LR
    요청(["📡 외부 API 호출"]) --> T{"⏱️ Timeout"}
    T -->|응답 없음| R{"🔄 Retry"}
    T -->|응답 수신| 성공(("✅ 정상 응답"))
    R -->|재시도 성공| 성공
    R -->|재시도 실패| F(["🛡️ Fallback"])
    F --> 저하(("⚠️ 성능 저하된\n서비스 제공"))
    FT(["🔘 Feature Toggle"]) -.->|장애 시 비활성화| 요청

    style 요청 fill:#e0f2fe,stroke:#38bdf8,color:#0c4a6e
    style T fill:#fef3c7,stroke:#f59e0b,color:#78350f
    style R fill:#fff7ed,stroke:#f97316,color:#7c2d12
    style 성공 fill:#f0fdf4,stroke:#22c55e,color:#14532d
    style F fill:#f3e8ff,stroke:#a855f7,color:#581c87
    style 저하 fill:#fce7f3,stroke:#ec4899,color:#831843
    style FT fill:#f1f5f9,stroke:#64748b,color:#334155

Layer 1: 타임아웃 제어

컴포넌트타임아웃이유
연결 설정5s외부 AI 서비스 연결
세션 생성7s세션 초기화 최대 대기
SSE 응답65sAI 스트리밍 최대 대기 (의도 분석 \→ RAG 검색 \→ 다중 Agent 응답)
DB 연결 획득3s커넥션 풀 대기
DB Idle 타임아웃30min미사용 연결 자동 정리

왜 SSE는 65s인가? 복잡한 쿼리는 Intent → RAG Search → Multiple agent responses를 거친다. 30~60초가 정상이므로 30초 이하로 설정하면 유효한 응답을 조기에 차단한다.

Layer 2: 재시도 정책

설정이유
최대 재시도2회3회 이상은 보통 영속적 장애
재시도 간격500ms일시적 오류 복구에 충분한 속도
재시도 대상네트워크 오류, 타임아웃, 일시적 5xx
1차 시도 → 실패 → 500ms 대기 → 2차 시도 → 실패 → Fallback

재시도하면 안 되는 경우

  • 4xx 오류 (클라이언트 오류, 재시도해도 소용 없음)
  • 비즈니스 로직 오류 (잘못된 입력, 권한 부족)
  • 65초 이상 타임아웃 (이미 충분히 대기했음)

Layer 3: Fallback (우아한 성능 저하)

기능별 Fallback 전략

기능Fallback사용자 영향
AI 응답”현재 AI Agent 응답이 지연되고 있습니다. 잠시 후 다시 시도해 주세요.”재시도 안내
대화 요약간단 요약 생성 (LLM 없음)품질 저하
이벤트 로깅로깅 스킵없음 (사용자에게 투명)
채팅 기록 저장저장 스킵, 응답만 전달없음
외부 시스템 동기화 (EAIP)상태 기록, 나중에 재시도동기화 지연

설계 원칙

  1. 핵심 기능 우선: 사용자 응답 전달 > 부수 작업
  2. 우아한 성능 저하: 완전 장애가 아닌 부분 장애
  3. 명확한 사용자 커뮤니케이션: 무슨 일이 있었고 어떻게 하면 되는지 안내

Layer 4: Feature Toggle

각 외부 시스템별 런타임 토글

기능토글비활성화 시
견적 조회 (Eloqua)eloqua.quotation.enabled제출 스킵, 성공 반환
로그 동기화 (EAIP)eaip.enabled동기화 스킵
AI 요약 (LLM)llm.enabled간단 요약 사용

왜 Circuit Breaker 대신 Feature Toggle인가?

  • Circuit Breaker는 자동 작동 (오탐지 가능)
  • Toggle은 운영팀의 명시적 제어
  • 예정된 유지보수 전에 사전 비활성화 가능
  • 프로덕션 인시던트 시 직관적 대응

외부 시스템 격리

시스템목적장애 영향격리 전략
AI Agent챗봇 응답높음 (핵심)재시도 + Fallback 메시지
Eloqua견적 제출중간Feature Toggle
EAIP로그 동기화낮음상태 기록 + 나중에 재시도
OpenAISTT 변환중간오류 메시지 반환

오류 응답 표준화

{
  "error": "AI_AGENT_ERROR",
  "message": "AI 응답 처리 중 오류가 발생했습니다",
  "timestamp": "2025-01-05T10:30:00"
}
오류 유형HTTP Status사용자 메시지
AI 서비스 오류500AI 응답 처리 중 오류 발생
견적 오류502견적 제출 중 오류 발생
입력 검증 실패400입력 값이 유효하지 않습니다
내부 오류500내부 서버 오류가 발생했습니다

WebFlux에서 부수 작업 격리

// ❌ Bad: 부수 작업이 메인 스트림을 블로킹
.doOnNext(message -> {
    aiChatLogService.save(message).block(); // Event Loop 블로킹!
})

// ✅ Good: 별도 스케줄러에서 Fire-and-Forget
.doOnNext(message -> {
    aiChatLogService.save(message)
        .subscribeOn(Schedulers.boundedElastic())
        .subscribe();
})

핵심 인사이트 — Reactive 스트림에서 비필수 작업은 절대 Event Loop를 블로킹해서는 안 된다. boundedElastic 스케줄러에서 Fire-and-Forget 패턴을 사용한다.

핵심 요약

  1. 성공이 아닌 실패를 설계한다 — 모든 외부 호출은 실패할 수 있다
  2. 데이터 완전성보다 사용자 응답을 우선한다 — 로깅 실패해도 응답을 전달한다
  3. 방어 계층을 중첩한다 — 타임아웃 → 재시도 → Fallback → 토글
  4. Feature Toggle을 Circuit Breaker보다 선호한다 — B2B는 운영팀의 명시적 제어가 필요하다
  5. 오류 응답을 표준화한다 — 모든 실패 모드에서 일관된 형식을 사용한다
  6. 부수 작업은 절대 블로킹하지 않는다 — 로깅, 동기화, 분석은 Fire-and-Forget으로 처리한다