TL;DR — 스스로 채점하고 고쳐 쓰는 에이전트도 결국 외부에 의존합니다. DB가 죽고, API가 타임아웃 나고, 응답 파싱이 깨지죠. 예외 처리와 복구는 이런 장애에도 에이전트가 죽지 않고 우아하게 대응하게 만드는 패턴 — 3단계(감지→처리→복구), 재시도+지수 백오프, 폴백 체인, Graceful Degradation, 서킷 브레이커입니다. 감지 → 처리(재시도·폴백·저하) → 복구(롤백·자가수정·에스컬레이션) 핵심은 "모든 에러를 잡는 것"이 아니라 재시도할 에러와 빨리 포기할 에러를 구분하는 것입니다.
1. 왜 예외 처리인가 — 점검하는 AI에서 넘어져도 일어서는 AI로
지난 편에서 에이전트는 자기 출력을 스스로 채점하고 합격선까지 고쳐 쓰는 능력을 얻었습니다. 그런데 그 "재시도"가 향하는 외부 세계는 언제든 무너집니다 — DB 서버 다운, LLM API 타임아웃, JSON 파싱 실패, 예측 못 한 입력. 예외 처리 없는 에이전트는 바로 그 순간 크래시합니다.
교재의 한 장면이 차이를 압축합니다. 사용자가 "내 주문 상태 알려줘"라고 했을 때, 예외 처리가 없으면 DB 조회 → 서버 다운 → 💥 크래시 → 사용자: ???로 끝납니다. 예외 처리가 있으면 재시도(3회) → 실패 → "잠시 후 다시 시도해주세요" → 관리자 알림으로 이어지죠. 예외 처리와 복구(Exception Handling and Recovery) 패턴은 에이전트에 안전장치를 다는 일입니다.
2025년 엔터프라이즈 AI가 이 패턴에 주목하는 이유는 분명합니다. 24/7 무중단(새벽 3시에 죽으면 안 됨), 비용 관리(무한 재시도 = 무한 API 비용), 신뢰성(95% → 99.9% 성공률), 규제 준수(금융·의료는 장애도 감사 로그 필수) — 네 가지를 동시에 요구하기 때문이죠.
2. 개념과 오해 — "예외를 잡는 것"과 "처리하는 것"은 다르다
📌 한 줄 정의 — AI 에이전트가 예기치 않은 오류를 감지하고, 적절히 대응하며, 안정적인 상태로 복구하는 능력을 갖추게 하는 패턴. 본질은 "예외가 발생해도 시스템이 죽지 않고, 가능한 한 정상 동작을 계속하거나 안전하게 종료하는 것"입니다.
주니어 개발자가 이 패턴에서 가장 많이 빠지는 오해 네 가지를 먼저 짚고 갑니다.
except: pass는 잡기만 할 뿐 아무것도 처리하지 않죠. 잡고 나서 무엇을 할지(재시도·폴백·알림)가 본질입니다.기억하기 쉬운 비유는 항공기 조종사의 체크리스트입니다. 계기판 경고등이 켜지면 문제를 감지(Detection), 상황별 비상 절차 매뉴얼로 대응(Handling), 대체 공항에 착륙하거나 회항해 안전 상태로 복귀(Recovery), 그래도 안 되면 관제탑에 비상 상황을 알려 인간 개입을 요청(Escalation)합니다. 에이전트도 똑같이 동작합니다.
3. 3단계 접근법 — Detection → Handling → Recovery
패턴의 골격은 3단계입니다. "무엇이 잘못됐나?"(감지) → "어떻게 대응할까?"(처리) → "정상으로 돌아가자"(복구). 색으로 역할을 구분해 보면 — 빨강은 문제 감지, 파랑은 대응 실행, 초록은 정상 복귀입니다.
파싱 실패 · 비즈니스 에러
LLM 환각
폴백 · Graceful Degradation
알림
자기 수정(Self-Correction)
에스컬레이션
2단계 처리가 다섯 갈래로 펼쳐지는 게 이 패턴의 핵심 두께입니다. 로깅(디버깅·감사), 재시도(일시 오류에 지수 백오프), 폴백(주 방법 실패 시 대체), Graceful Degradation(전체 실패 대신 부분 기능), 알림(운영자·다른 시스템에 통지) — 어느 한 갈래에서 막혀도 다음 갈래가 받아냅니다. 이 다층 방어(Defense in Depth)가 4절의 핵심 원칙으로 이어집니다.
4. 에러 분류가 먼저다 — 재시도할 에러 vs 포기할 에러
"어떻게 대응할까?"의 답은 에러 종류에 따라 다릅니다. 인증 실패를 재시도하는 건 시간·비용 낭비고, 네트워크 끊김을 영구 실패로 처리하는 건 과민 반응이죠. 그래서 처리 이전에 분류가 옵니다. 같은 키로 전략이 갈립니다.
| 에러 유형 | 예시 | 처리 전략 |
|---|---|---|
| TRANSIENT (일시) | 네트워크 끊김, 서버 일시 장애 | 재시도 (지수 백오프) |
| RATE_LIMIT | API 호출 한도 초과 | 대기 후 재시도 (더 긴 대기) |
| TIMEOUT | 응답 시간 > 30초 | 타임아웃 늘려 재시도 |
| PARSE_ERROR | JSON 파싱 실패, 스키마 불일치 | 형식 변경 후 재생성 |
| PERMANENT (영구) | 인증 실패, API Key 오류 | 재시도 금지 → 즉시 에스컬레이션 |
| BUSINESS | "잔액 부족", "권한 없음" | 사용자에게 안내 (자기 수정) |
이 분류 위에 세 가지 핵심 원칙이 얹힙니다. 이 셋이 "에러를 잡는 코드"를 "복원력 있는 시스템"으로 끌어올립니다.
5. 구현 ① 순수 Python — 분류·데코레이터·폴백 체인
프레임워크 없이 Gemini API만으로 패턴의 뼈대를 봅니다. 설계 철학은 분류 우선(처리 전에 에러를 나눈다), 책임 분리(Detection/Handling/Recovery를 명확히), 데코레이터 활용(재시도 로직을 비즈니스 로직과 분리)입니다. 먼저 에러를 유형으로 못 박고 분류 함수를 둡니다.
재시도는 데코레이터로 분리합니다. 비즈니스 함수는 깨끗이 두고, @retry_with_backoff만 붙이면 지수 백오프가 자동으로 걸리죠. 대기 시간 계산과 지터는 7절에서 따로 깊게 봅니다.
마지막으로 폴백 체인입니다. 주 모델이 실패하면 백업 모델, 그것도 실패하면 캐시된 응답 — 성공할 때까지 순차 시도합니다. FallbackChain에 핸들러를 등록하고 execute()만 부르면 됩니다(폴백 설계는 8절에서 깊게).
여기에 Graceful Degradation(필수/선택 컴포넌트 구분)과 StateManager(checkpoint()/rollback())까지 더하면 기본기는 완성입니다. 다만 재시도·폴백·분기·상태가 데코레이터·지역 변수·if문에 흩어져 있어, 노드가 늘고 흐름이 복잡해지면 추적이 어렵습니다. 그래서 LangGraph가 등장합니다.
6. 구현 ② LangGraph — State에 에러를 새기는 복원력 그래프
2025년 LangChain 팀이 권장하는 LangGraph는 에러 정보를 State에 저장해 모든 노드에서 추적합니다(State-Driven Error Tracking). 재시도·복구·에스컬레이션은 if문이 아니라 조건부 엣지로 선언하죠. 먼저 에러를 품은 상태를 정의합니다.
도구 실행 에러는 ToolNode with Fallback으로 잡습니다. 예외를 삼켜 ToolMessage로 바꿔 LLM에게 돌려주면, LLM이 에러를 인식하고 다른 접근을 시도(자기 수정)할 수 있죠.
마지막으로 노드를 조건부 엣지로 엮습니다. recovery 노드는 재시도 가능하면 agent로 되돌리고, Critical이면 escalation으로, 일반 실패면 종료합니다 — 재시도 루프와 에스컬레이션이 그래프로 표현됩니다.
5절의 try/except가 노드와 엣지로, 데코레이터 재시도가 recovery→agent 루프로, 지역 변수가 State로 바뀌었습니다. 그 대가로 상태 추적·시각화·체크포인트 재시작(실패 지점부터 resume)이 전부 따라옵니다.
7. 백오프와 서킷 브레이커 — 재시도가 독이 되지 않게
재시도는 양날의 검입니다. 잘 쓰면 일시 장애를 넘기지만, 잘못 쓰면 죽어가는 서버에 부하를 더해 완전히 죽입니다. 두 가지 안전장치가 이를 막습니다 — 지수 백오프(점점 더 오래 기다림)와 서킷 브레이커(연속 실패 시 아예 차단).
여기에 지터(Jitter)를 더합니다. 같은 순간 장애가 나면 모든 클라이언트가 동시에 재시도하는 Thundering Herd 문제가 생기는데, 대기 시간을 0.5~1.5배로 무작위 분산하면 부하가 흩어집니다.
delay *= 0.5 + random.random()마지막 장치는 서킷 브레이커(Circuit Breaker)입니다. 전기 회로의 두꺼비집처럼, 연속 실패가 임계치를 넘으면 회로를 열어(OPEN) 더 이상 호출하지 않고 즉시 실패시킵니다. 일정 시간 뒤 HALF-OPEN으로 한 번 시험 호출을 보내, 성공하면 CLOSED(정상)로 복귀하죠. 적용전략의 RetryEngine은 재시도가 한계에 닿을 때 circuit_breaker.record_failure()를 호출하고, 회로가 열려 있으면 재시도 자체를 건너뛰어 재시도 폭주를 원천 차단합니다.
8. 폴백과 우아한 저하 — 전부 아니면 전무를 거부한다
재시도가 시간 축의 방어라면, 폴백 체인은 공간 축의 방어입니다. 주 방법이 막히면 다음 대안으로, 그것도 막히면 그다음으로 — LLM 서비스 장애를 상정한 다단계 폴백입니다.
(Gemini)
(GPT-4o)
응답
연결
폴백이 "대안을 갈아탄다"면, Graceful Degradation은 "전체 대신 부분이라도 살린다"입니다. 핵심은 컴포넌트를 필수(Required)와 선택(Optional)으로 나누는 것 — 필수가 실패하면 전체 실패지만, 선택이 실패하면 경고만 남기고 계속 진행합니다.
| 컴포넌트 | 구분 | 실패 시 동작 |
|---|---|---|
| 고객 기본 정보 | 필수 | 전체 실패 → 즉시 반환 |
| 주문 이력 | 선택 | 경고 추가 + None으로 계속 |
| 추천 상품 | 선택 | 기본 추천으로 대체 |
결과는 "내 정보 보여줘"에 전체 에러 대신 기본 정보 + ⚠️ "주문 이력은 잠시 후 확인해주세요"를 돌려주는 것. 사용자는 부분적이라도 답을 받습니다.
9. 두 구현 비교 — 같은 복원력, 다른 추상화
순수 Python과 LangGraph는 같은 복원력 패턴을 구현하지만 추상화 수준이 다릅니다. 단순한 호출이면 Python이 빠르고, 노드가 늘거나 재시작·시각화·HITL이 필요하면 LangGraph가 유리합니다.
| 측면 | ① 순수 Python | ② LangGraph |
|---|---|---|
| 에러 추적 | 예외 + 데코레이터 | State-Driven(error_history) |
| 재시도 | @retry_with_backoff |
recovery→agent 루프 |
| 분기 | if-else | Conditional Edge |
| 상태 | StateManager(수동) | StateGraph (TypedDict) |
| 폴백 | FallbackChain | ToolNode.with_fallbacks |
| 복구 재시작 | 수동 checkpoint/rollback | Checkpointer(자동 resume) |
| 시각화 | 불가 | Mermaid 지원 |
| 학습 곡선 | 낮음 | 중간 |
세 번째 선택지로 Google ADK도 있습니다. 교재가 소개한 패턴은 SequentialAgent(sub_agents=[primary_handler, fallback_handler, response_agent]) — 1차 핸들러 실패 시 2차 핸들러로 넘기는 폴백을 에이전트 조합으로 표현하죠. 핵심은 어느 추상화든 결국 "재시도·폴백·복구"라는 같은 골격을 담는다는 점입니다.
10. 엔터프라이즈 — CRM·ERP·Groupware의 복원력 ROI
레거시 시스템의 고통점은 공통적으로 "장애 한 번에 전부 멈춘다"입니다. CRM은 외부 서비스 장애 시 고객 응대 불가, ERP는 배치 중 오류 나면 전체 롤백(부분 처리 불가), Groupware는 외부 캘린더 연동 실패 시 핵심 기능 마비. 복원력 패턴은 이를 다층 방어 + 부분 복구로 바꿉니다.
| 시스템 | 핵심 적용 | 기대효과(목표치) |
|---|---|---|
| CRM 고객 상담 |
Retry + 폴백 체인(백업 LLM→템플릿→상담원) | 가용성 99.95%(월 22분↓) · 자동 복구 85% · 이탈률 5% 미만 |
| ERP 배치 처리 |
Checkpoint + 건별 격리(전체 롤백 회피) | 부분 처리 99% · 복구 4시간→30분 · 자동 복구 70% |
| Groupware 일정 연동 |
다중 캘린더 폴백 + 지연 동기화 | 연동 성공률 80%→99.5% · NPS +20 · 동기화 99.9% |
기술 스택은 LangGraph(워크플로우), Gemini·GPT(다중 Provider 폴백), Redis(체크포인트), LangSmith+Grafana(관측), PagerDuty(알림)가 권장 조합입니다. 핵심 성공 요인은 에러 분류 정확성(Transient vs Permanent), 지수 백오프 + 지터, 3단계 이상 폴백, 체크포인트 재시작, 그리고 Critical 에러의 Human-in-the-Loop입니다.
11. 케이스 스터디 — CRM 상담봇, DB 장애를 견디다
DB 서버가 일시 장애인 상황에서, 고객이 "내 주문 #12345 배송 상태 알려줘"라고 묻습니다. 3단계가 어떻게 한 흐름으로 작동하는지 따라가 봅니다.
ConnectionError 발생 → TRANSIENT로 분류고객은 크래시 화면 대신 유용한 대체 응답을 받았고, 운영팀은 즉시 인지했으며, 시스템은 사람 손 없이 정상화됐습니다. "장애가 없었던 것"이 아니라 "장애가 사용자에게 보이지 않은 것" — 이것이 복원력의 목표입니다.
12. 베스트 프랙티스 — 언제 무엇을 쓸까
예외 처리는 "많이 넣을수록 좋은 것"이 아닙니다. 복구 가능한 에러에는 적극적으로, 복구 불가능한 에러에는 빠르게 포기하는 균형이 핵심입니다.
순서대로 쪼개고(체이닝), 갈래를 나누고(라우팅), 병렬로 돌리고(병렬화), 스스로 비평하고(리플렉션), 도구를 쥐고(도구 사용), 계획하고(플래닝), 팀을 이루고(멀티 에이전트), 기억하고(메모리), 배우고(학습), 표준으로 연결되고(MCP), 스스로 채점하던(목표 설정) 에이전트 — 이번엔 넘어져도 다시 일어서는 법(예외 처리·복구)을 배웠습니다. 다음 편은 Human-in-the-Loop: 자동화가 멈춰야 할 그 순간, 사람을 어떻게 설계에 끼워 넣는가입니다.
- LangGraph 공식 문서 — 에러 핸들링·ToolNode·Checkpointing
- Tenacity — Python 재시도(Retry) 라이브러리 (지수 백오프·지터)
- Google Gemini API 문서 — 모델 호출·오류 응답
'Agentic AI Design Patterns' 카테고리의 다른 글
| CH14-환각 대신 근거로 말하게 하라 — AI 에이전트 검색 증강 생성(RAG) 설계 (0) | 2026.06.10 |
|---|---|
| CH13-AI가 계정을 멋대로 삭제하지 못하게 하는 법 — 위험도 기반 에스컬레이션과 인간 승인(HITL) 패턴 (1) | 2026.06.10 |
| CH11-스스로 채점하는 AI — 목표 설정과 모니터링(Goal Setting & Monitoring) 완벽 정리 (0) | 2026.06.07 |
| CH10-MCP는 '또 다른 API'가 아니다 — 표준 프로토콜로 모든 LLM에 외부 시스템을 연결하는 법 (1) | 2026.06.07 |
| CH09-기억하는 AI는 끝났다 — 이제 '배우는 AI'다 (피드백 루프부터 자기 개선까지) (0) | 2026.06.06 |