LangGraph의 결함 허용(Fault Tolerance): 재시도, 타임아웃 및 에러 핸들러
요약
LangGraph를 사용하여 프로덕션 환경의 AI 에이전트에서 발생하는 네트워크 장애, 도구 호출 오류, LLM 속도 제한 등을 처리하는 방법을 다룹니다. 재시도 정책, 타임아웃 정책, 에러 핸들러라는 세 가지 핵심 요소를 통해 에이전트의 결함 허용 능력을 높이는 가이드를 제공합니다.
핵심 포인트
- LangGraph는 재시도, 타임아웃, 에러 핸들링을 일급 시민으로 지원함
- RetryPolicy를 통해 일시적 오류에 대한 자동 재시도 구현 가능
- TimeoutPolicy로 노드 실행 시간 및 진행 상황 제한 가능
- 에러 핸들러를 통해 재시도 실패 시 컨텍스트를 포함한 후속 처리 가능
.png)
실제 환경에서 에이전트(Agents)는 프로토타입에서는 결코 볼 수 없는 오류들을 마주합니다: 네트워크 장애, 도구 호출(Tool call) 오류, LLM 속도 제한(Rate limits) 등이 그것입니다.
몇 시간 또는 며칠 동안 실행 중인 작업이 중간에 복구 불가능한 오류에 부딪혔다고 상상해 보십시오. 어떻게 하시겠습니까? 실행을 포기하고 완전히 처음부터 다시 시작하시겠습니까? 이는 프로덕션(Production) 에이전트를 운영하는 지속 가능한 방식이 아닙니다.
해피 패스(Happy path)를 작성하는 것은 대개 쉬운 부분입니다. 프로덕션에서 살아남을 수 있게 만드는 에러 핸들링(Error handling) 보일러플레이트(Boilerplate)(재시도(Retries), 타임아웃(Timeouts), 폴백(Fallbacks))는 종종 비즈니스 로직 자체보다 더 길어지곤 합니다.
LangGraph는 이를 일급 시민(First-class) 관심사로 취급합니다. LangGraph는 에이전트를 그래프로 구성된 일련의 개별 단계(노드(Nodes))로 모델링하는 저수준 오케스트레이션 프레임워크(Low-level orchestration framework)입니다. 일반적인 에이전트의 경우, 모델을 호출하는 노드와 모델이 반환한 도구 호출을 실행하는 노드, 그리고 해당 루프를 감싸고 싶은 결정론적 로직(Deterministic logic)이 포함됩니다. LangGraph는 실행을 제어하기 때문에, 이러한 단계 중 하나가 실패했을 때 발생하는 상황을 처리하기에도 적합한 장소입니다.
이 포스트에서는 LangGraph가 해당 보일러플레이트를 도와주는 세 가지 기본 요소(Primitives)를 살펴봅니다. 이 요소들이 어떻게 결합되는지, 그리고 특히 정리/보상 로직(Cleanup / compensation logic)을 고민하기 시작할 때 왜 워크플로 엔진(Workflow engine) 내부에 이들을 두는 것이 중요한지 다룹니다. (문서 참조: Fault tolerance.)
세 가지 기본 요소는 다음과 같습니다:
RetryPolicy (재시도 정책): 일시적인 오류(Transient errors)에 대해 백오프/지터(Backoff/jitter)를 적용한 자동 재시도.
TimeoutPolicy (타임아웃 정책): 노드 시도에 대한 실제 시간(Wall-clock) 또는 진행 상황 기반의 제한.
error_handler (에러 핸들러): 재시도가 모두 소진된 후, 실패 컨텍스트(Failure context)가 첨부된 상태로 실행되는 노드.
LangGraph에서 에이전트는 StateGraph에 노드와 엣지(Edges)를 추가함으로써 정의됩니다. 세 가지 기본 요소 모두 add_node를 통해 노드에 직접 연결되므로, 결함 허용(Fault tolerance) 설정이 보호하려는 로직 바로 옆에 위치하게 됩니다. (기본값을 한 번에 설정하고 싶다면 set_node_defaults를 참조하세요.)
from langgraph.graph import StateGraph
from langgraph.types import RetryPolicy, TimeoutPolicy
from langgraph.errors import NodeError
...
재시도(Retries)부터 시작하기
일시적인 실패(Transient failures)는 복잡한 그래프에서 가장 흔하게 발생하는 실패 유형입니다. LLM 제공업체가 5xx 에러를 반환하거나, 벡터 스토어(Vector store)에서 연결 재설정(Connection reset)이 발생하거나, 다운스트림 HTTP 서비스가 잠시 사용 불가능해지는 경우 등이 이에 해당합니다. 이 모든 사례는 근본적으로 "잠시 후에 다시 시도하면 아마도 작동할" 종류의 오류입니다.
일급 객체(First-class) 지원이 없다면, 결국 모든 노드 내부에 동일한 래퍼(Wrapper)를 작성하게 됩니다:
def call_llm(state):
# ~25줄에 달하는 "지수 백오프(Backoff)를 적용하여 재시도하되, 5xx 에러에 대해서만 수행하고,
# 4xx 에러에는 재시도하지 않으며, 각 시도를 로그로 남기고, 지터(Jitter)를 포함하여 대기하라"는 로직
...
LangGraph의 RetryPolicy는 이러한 상용구 코드(Boilerplate)를 제거합니다. 이는 노드 시도(Per node attempt) 단위로 적용되며, 지수 백오프(Exponential backoff), 선택적 지터(Jitter), 그리고 어떤 예외를 재시도 가능한 것으로 간주할지에 대한 설정 가능한 서술어(Predicate)를 제공합니다:
from langgraph.types import RetryPolicy
policy = RetryPolicy(
initial_interval=0.5,
...
기본 retry_on 설정은 의도적으로 보수적입니다. 이는 ConnectionError, httpx/requests의 5xx 응답, 그리고 몇 가지 일반적인 일시적 범주에 대해서만 재시도를 수행합니다.
기본적으로 ValueError, TypeError, RuntimeError 등은 재시도하지 않는데, 이러한 오류들은 거의 항상 프로그래밍 버그이기 때문입니다.
retry_on 명세는 에러 유형의 컬렉션(Collection)이거나, 런타임에 에러를 확인하여 재시도 기준에 부합하는지 검사하는 호출 가능한 객체(Callable)일 수 있습니다.
타임아웃(Timeout): "일시적 실패"의 특수한 사례
타임아웃은 사실상 "시도가 너무 오래 지속되었기 때문에 일시적 실패로 간주하는 것"입니다. 명시적인 타임아웃이 없다면, 멈춰버린 HTTP 호출이나 얼어붙은 서브프로세스(Subprocess)가 그래프 실행을 무기한 중단시킬 수 있습니다.
LangGraph의 TimeoutPolicy는 두 가지 유형의 타임아웃을 지원합니다:
from langgraph.types import TimeoutPolicy
TimeoutPolicy(
run_timeout=30.0, # 단일 시도에 대한 엄격한 실제 시간(Wall-clock) 제한
...
run_timeout
run_timeout
은 단일 시도에 대한 엄격한 실제 시간(Wall-clock) 제한입니다. 노드에 대해 N초 이상 기다리는 것을 원치 않을 때 유용합니다.
idle_timeout
은 모든 "진행(progress)" 신호 시 채널 쓰기(channel writes), 스트리밍 청크(streamed chunks, LangChain LLM 모델에서 자동으로 방출됨), 자식 태스크 이벤트(child task events), LangChain 콜백 이벤트 발생 시 초기화됩니다. 실행 시간이 길더라도 활발하게 스트리밍 중인 작업은 이 타임아웃을 트리거하지 않지만, 진정으로 멈춰버린(hung) 호출은 트리거합니다.
-
내부적으로는 모든 신호에 대해 "하트비트(heartbeat)"에 의존합니다. 만약 작업(work)을 직접 제어하고 자체적인 진행 하트비트를 방출할 수 있다면,
refresh_on="heartbeat"로 전환하고 노드 내부에서runtime.heartbeat()를 명시적으로 호출할 수 있습니다. -
내부적으로는 모든 신호에 대해 "하트비트(heartbeat)"에 의존합니다. 만약 작업(work)을 직접 제어하고 자체적인 진행 하트비트를 방출할 수 있다면,
refresh_on="heartbeat"로 전환하고 노드 내부에서runtime.heartbeat()를 명시적으로 호출할 수 있습니다.
타임아웃이 발생하면 노드 시도가 취소되고 NodeTimeoutError가 발생합니다.
에러 핸들러(Error handlers): 재시도만으로 충분하지 않을 때
재시도(Retries)는 "5초 뒤에는 아마 작동할 것입니다"와 같은 상황을 처리합니다. 하지만 재시도가 모두 소진되었을 때 특정 로직을 실행해야 하는 경우에는 대응할 수 없습니다. 예를 들어, "6번 시도했지만 결제 제공업체가 여전히 다운 상태이므로, 이제 다음을 수행해야 합니다:
- 주문을 실패로 표시하고 고객에게 알림을 보내거나,
- 이미 커밋된 부분적인 부수 효과(side effects)를 롤백(roll back)하거나,
- 시스템의 나머지 부분이 반응할 수 있도록
payment.failed이벤트를 발행해야 합니다."
재시도 소진 후 에러 핸들러를 사용하는 사례는 매우 많습니다. 여기에는 정리(cleanup), 경고(alerting), 데드 레터 쓰기(dead-letter writes), 더 저렴한 모델로의 폴백 경로(fallback paths), 또는 단순히 "죄송합니다" 메시지로 라우팅하는 것이 포함됩니다.
LangGraph에서는 이제 이를 자연스럽게 지원합니다 (문서: Error handling):
from langgraph.errors import NodeError
def on_call_llm_failed(state: State, error: NodeError) -> State:
log.error("call_llm failed after retries:%s", error.error)
...
이것이 어떻게 연결(wired)되어 있는지에 대해 주목해야 할 몇 가지 사항이 있습니다:
재시도가 모두 소진된 후에만 실행됩니다. 이것이 이 기능이 실제로 유용하게 만들어주는 속성입니다. 만약 모든 예외(exception)마다 실행되기를 원한다면, 노드(node) 내부에 try/except를 작성하기만 하면 됩니다.
실패 컨텍스트(failure context)가 주입됩니다. 핸들러(handler)는 NodeError를 매개변수로 사용하여 실패한 노드의 이름과 예외(error.node, error.error)를 가져올 수 있습니다.
전환(transition)은 원자적(atomic)입니다. 원래의 노드가 실패하면, 해당 노드의 ERROR 쓰기 작업은 체크포인트(checkpoint)에 커밋되며, 핸들러 태스크(task)는 동일한 스텝(step) 내의 새로운 태스크로 스케줄링됩니다. 이는 에러 핸들러 스텝에 진입한 후 일반 스텝으로 되돌아갈 수 없는 일부 중요한 프로세스에서 매우 중요합니다. 만약 핸들러 실행 도중 호스트 프로세스가 충돌(crash)하더라도, 다음에 실행을 재개할 때는 원래 실패했던 노드가 아니라 핸들러를 다시 스케줄링합니다.
에러 핸들러는 동일한 실행 사이클(execution cycle) 내에서 실행됩니다.* 노드가 실패하면, 에러 핸들러는 해당 스텝에서 이미 실행 중이던 다른 노드들과 함께 즉시 스케줄링됩니다. 핸들러가 다른 노드들이 끝나기를 기다리지 않으며, 다른 노드들도 핸들러를 기다리지 않습니다.
*LangGraph에서는 런타임(runtime)에 익숙하시다면
단순한 접근 방식(전체를 그냥 다시 시도하는 것)은 금방 한계에 부딪힙니다. 만약 좌석 예약은 성공했지만 결제나 티켓 발권이 실패한다면, 예약은 잘못된 상태(bad state)에 빠지게 됩니다. 실제로 필요한 것은 각 단계를 개별적으로 재시도하는 것이며, 특정 단계가 재시도 횟수를 모두 소진하면 이미 실행된 단계들(상태를 알 수 없는 실패한 단계 포함)만 취소(undo)하는 것입니다.
이를 SAGA 패턴이라고 부르며, 모든 과정을 단일 데이터베이스 트랜잭션(database transaction)으로 묶을 수 없는 분산 시스템(distributed systems)에서 실패를 처리하는 표준적인 방법입니다.
LangGraph에서는 다음과 같이 구현됩니다:
from typing import TypedDict, Annotated, Literal
import operator
from langgraph.graph import StateGraph, START, END
...
이를 통해 얻을 수 있는 이점은 다음과 같습니다:
- 설정된 정책에 따른 단계별 백오프 재시도 (Per-step backoff retries)
- 어떤 단계의 재시도가 소진되면
compensate단계로의 원자적 전이 (An atomic transition intocompensate) - 어떤 단계가 실제로 완료되었는지에 대한 지속적인 상태 추적 (Persistent state tracking)을 통해,
compensate가 되돌려야 할 부분만 취소하도록 보장
마치며
에이전트(Agents)는 점점 더 많은 자율성을 갖게 되고 있으며, 그에 따라 행동할 수 있는 권한도 커지고 있습니다. 에이전트들은 항공권을 예약하고, 티켓을 접수하며, 결제를 실행하고, 내부 서비스를 호출합니다. 에이전트가 취하는 행동들은 점점 더 결과가 중대해지고 되돌리기 어려워지고 있습니다.
이는 신뢰성(reliability)에 대한 기준을 높입니다. 데모 환경에서의 1% 일시적 오류율(transient failure rate)은 사소한 불편함에 불과하지만, 수십 개의 단계와 실제적인 결과가 따르는 프로덕션 에이전트 환경에서는 그 문제가 빠르게 누적됩니다.
RetryPolicy, TimeoutPolicy, 그리고 error_handler는 LangGraph에 내장되어 있어, 온갖 종류의 오류에 탄력적으로 대응하는(resilient) 에이전트를 쉽게 구축할 수 있습니다. 여러분은 사용 사례에 맞는 정책을 정의하기만 하면 되며, 나머지는 LangGraph 에이전트 런타임(agent runtime)이 처리합니다.
시작하기: 공식 결함 허용(Fault tolerance) 문서를 통해 노드별 재시도, 타임아웃 및 에러 핸들러를 설정해 보세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 LangChain Blog의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기