RAG2026-5일31차시 · 예외 처리 설계11 / 19← 이전목차다음 →MetaContents

31차시. 예외 처리 설계

8. 실습 ④ — 장애 시뮬레이션

출처: 원본 PDF 31차시_방어적코딩실습.pdf 17~18쪽 · 쪽 그림 17쪽 18쪽

실제로 고장을 내보고 시스템이 버티는지 확인합니다.

📝 src/test_failure.py

출력
from unittest.mock import patch
from graph import ask

def test_case(name, target, exception):
    print(f"\n{'='*55}\n[{name}]")
    with patch(target, side_effect=exception):
        try:
            r = ask("환불은 며칠 이내인가요?")
            print(f"✓ 시스템 생존")
            print(f"  답변: {r['answer'][:70]}")
            print(f"  검증: {r.get('verified')}")
        except Exception as e:
            print(f"✗ 시스템 중단! {type(e).__name__}: {e}")

# 1) 검색 실패
test_case("인덱스 오류",
"nodes.retriever._store.similarity_search_with_relevance_scores",
          RuntimeError("index corrupted"))

# 2) 생성 실패
test_case("LLM API 오류", "nodes.generator._chain",
          TimeoutError("request timeout"))

# 3) 검증 실패
test_case("JSON 파싱 실패", "nodes.verifier._judge",
          ValueError("invalid response"))

실행 결과
═════════════════════════════════════════════
══════════
[인덱스 오류]
✓ 시스템 생존
  답변: 일시적인 시스템 오류가 발생했습니다. 잠시 후 다시...
  검증: False

[LLM API 오류]
✓ 시스템 생존
  답변: 답변 생성에 실패했습니다. 대신 관련 자료를 보여드립니다...
  검증: False

[JSON 파싱 실패]
✓ 시스템 생존
  답변: 환불은 상품 수령 후 7일 이내에 신청하실 수 있습니다[1].
  검증: False          ← 검증만 건너뛰고 답변은 제공!

🎯 세 번째 케이스가 핵심입니다
검증이 실패했지만 답변은 정상적으로 제공됐습니다.
그리고 verified=False로 표시했습니다.
예외 처리가 없었다면
사용자는 500 에러만 받았을 겁니다.
답변이 이미 만들어져 있었는데도요.

실습 코드 · 31/test_failure.py

실습 코드: 실습/src/31/test_failure.py · 위의 원본 PDF 31차시 17~18쪽 「8️⃣ 실습 ④ — 장애 시뮬레이션」과 이어집니다.

Python
# ==============================================================
# 31차시. PPT 31차시_예외처리설계. 코드 폴더는 src/31
# PPT: 31차시 안전 실행과 운영 로그
# 역할: retriever_node 자리에 항상 예외를 내는 함수를 넣어, safe_node가 그래프를 살리는지 봅니다.
# 읽는 키: question
# 쓰는 키: graph31.ask의 반환 중 node_error를 출력
# 그래프:
#   classify --> retrieve(일부러 오류) --> safe_node가 node_error 기록 --> 이후 분기
# --------------------------------------------------------------
# 파이썬만 아는 분을 위한 말
# State는 노드들이 함께 쓰는 딕셔너리입니다. 함수는 state를 읽고, 바꿀 키만 return 합니다.
# return에 적지 않은 키는 이전 값이 남습니다.
# log와 tried_queries는 리스트라서, 새로 return한 항목이 뒤에 이어 붙습니다.
# Retriever: 검색 담당. 질문과 비슷한 문서 조각만 고릅니다. 답 문장은 만들지 않습니다.
# Generator: 고른 문서를 근거로 답 문장을 만듭니다. 문서가 없으면 안내 문장만 돌려줍니다.
# Verifier: 답이 근거와 맞는지 검사하고 grade를 적습니다.
#   grade=pass     답을 확정하고 그래프를 끝냅니다.
#   grade=retry    같은 문서로 답 문장만 다시 씁니다. retries가 1 늘어납니다.
#   grade=research 문서가 부족합니다. 검색어를 바꾼 뒤 다시 찾습니다. rewrites가 늘어납니다.
#   grade=giveup   더 진행하지 않고 fallback으로 안내 문장만 돌려줍니다.
# fallback: 실패했을 때 답을 지어내지 않고 안내 문장을 반환하는 노드입니다.
# intent: 질문 종류. greeting 인사, calc 계산, scope 문서 밖, document 문서 검색.
# rewrite: 사용자가 친 question은 유지하고, 검색에 쓰는 query만 바꿉니다.
# safe_node: 노드를 try로 감쌉니다. 오류가 나도 그래프는 멈추지 않고 node_error에 적습니다.
# unittest.mock.patch로 graph31.retriever_node 이름만 잠깐 바꿉니다. 원본 파일은 수정하지 않습니다.
# broken_node는 RuntimeError('일부러 발생시킨 오류')를 일으킵니다.
# 여기까지 코드가 진행되면 print('시스템 생존')이 실행됩니다.
# graph31 import가 FAISS와 API 준비를 하므로 이 파일은 실행하지 않았습니다.
# ==============================================================

# ============================================================
# Safe Node 장애 테스트
# Retriever에서 일부러 오류를 발생시킵니다.
# 오류가 발생해도 전체 그래프가 중단되지 않는지 확인합니다.
# ============================================================

from unittest.mock import patch

import graph31

# ============================================================
# 테스트용 오류 노드
# 실행하면 무조건 오류를 발생시킵니다.
# ============================================================

def broken_node(state):

    # 실제 장애 상황을 가정하여 오류를 발생시킵니다.
    raise RuntimeError(
        "일부러 발생시킨 오류"
    )

# ============================================================
# Retriever 장애 테스트
# ============================================================

# graph31의 retriever_node를 broken_node로 잠시 교체합니다.
with patch(
    "graph31.retriever_node",
    broken_node
):

    # 평소처럼 질문을 실행합니다.
    result = graph31.ask(
        "환불은 며칠 이내인가요?"
    )

    # --------------------------------------------------------
    # 실행 결과 확인
    # --------------------------------------------------------

    # 여기까지 실행되면 그래프가 중단되지 않은 것입니다.
    print("시스템 생존")

    # safe_node가 잡은 오류 내용을 출력합니다.
    print(
        "오류:",
        result["node_error"]
    )

실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)

출력
===== 실행 결과 샘플 =====
경로: classify → retrieve(오류) → fallback → END
예시 출력 (API 키 필요, 실행하지 않음)
코드에 적힌 출력 문구:
시스템 생존
오류: 일부러 발생시킨 오류
safe_node가 예외를 잡으면 node_error에 그 문자열이 들어갑니다.
검색 점수는 이 테스트의 출력이 아닙니다.

코드 해설

retriever_node 자리에 항상 예외를 내는 함수를 넣어, safe_node가 그래프를 살리는지 봅니다.

코드 앞에서 고정해 두는 말입니다.

  • 읽는 키: question
  • 쓰는 키: graph31.ask의 반환 중 node_error를 출력
  • 그래프:
  • classify --> retrieve(일부러 오류) --> safe_node가 node_error 기록 --> 이후 분기
  • 파이썬만 아는 분을 위한 말
  • State는 노드들이 함께 쓰는 딕셔너리입니다. 함수는 state를 읽고, 바꿀 키만 return 합니다.
  • return에 적지 않은 키는 이전 값이 남습니다.
  • log와 tried_queries는 리스트라서, 새로 return한 항목이 뒤에 이어 붙습니다.

함수와 클래스로 나누어 읽습니다.

  • broken_node: 이름 그대로 한 가지 일만 합니다. 본문 주석을 같이 읽습니다.

파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.

← 이전 절31차시 · 7. 실습 ③ — 운영 로그 남기기다음 절 →31차시 · 9. 자주 겪는 문제