LangGraph 에이전트가 몇 주간의 조용한 데이터 손실 끝에 마침내 상태를 유지할 수 있는 신뢰할 수 있는 방법을 찾았습니다. SQLite, 원시 오브젝트 스토리지, 그리고 각각의 결함이 있는 버전이라는 세 번의 체크포인팅 시도 끝에, 저자는 요청이 들어올 때마다 에이전트가 처음부터 다시 시작하는 것을 방지하는 원자적 업데이트(atomic-update) 패턴을 찾아냈습니다.
LangGraph에서 체크포인팅이 중요한 이유
LangGraph는 개발자가 LLM 호출을 재사용 가능한 “에이전트”로 엮어 대화의 이전 내용을 기억할 수 있게 해줍니다. 이러한 에이전트는 사용자 요청을 하위 작업으로 나누고, 중간 결과를 저장하며, 다음 호출 시 중단된 지점부터 다시 시작합니다. 저장된 상태가 사라지면 에이전트는 모든 것을 다시 계산하여 컴퓨팅 자원을 낭비하고, 지연 시간을 높이며, 사용자 경험을 저하시킵니다. Telegram 메시지를 처리하는 프로덕션 봇의 경우, 데이터 손실로 인해 몇 주간의 대화 기록이 삭제되기도 했습니다.
첫 번째 해결책: SQLite saver
내장된 SqliteSaver는 단일 인스턴스가 에이전트를 실행할 때는 잘 작동합니다. 각 체크포인트를 로컬 SQLite 파일에 JSON 블롭(blob)으로 기록합니다. 문제는 개발자가 AgentState 타입에 새로운 필드를 추가하고 다시 배포했을 때 발생했습니다. 스키마 변경 전에 생성된 기존 체크포인트에는 새 필드가 없었습니다. SqliteSaver는 마이그레이션을 실행하지 않기 때문에, LangGraph는 불완전한 JSON을 로드하고 누락된 데이터를 버렸으며, 에이전트는 처음부터 다시 시작되었습니다.
핵심 포인트: SQLite 스토리지는 데모용 도구이며, 스키마 진화(schema evolution)가 필요한 프로덕션 환경을 위한 솔루션이 아닙니다.
두 번째 해결책: 오브젝트 스토리지
직렬화 형식을 제어하기 위해 저자는 JSON 체크포인트를 Oracle Cloud Object Storage에 업로드하는 커스텀 세이버(saver)를 작성했습니다. 이 방식은 스키마를 수동으로 버전 관리할 수 있는 유연성을 제공했지만, 새로운 장애 모드를 유발했습니다. 두 개의 요청이 동일한 대화 스레드에 동시에 도달하면, 두 요청 모두 동일한 오브젝트를 덮어쓰려고 시도합니다. 오브젝트 스토리지 서비스는 '한 번 쓰기, 여러 번 읽기(write-once, read-many)' 패턴에 최적화되어 있으며, 원자적 덮어쓰기(atomic overwrite) 의미론을 제공하지 않습니다. 이 경쟁 상태(race condition)로 인해 형식이 잘못되었거나 잘린 JSON 파일이 생성되었고, 에이전트는 다시 컨텍스트를 잃었습니다.
핵심 포인트: 여러 워커가 동시에 동일한 키에 접근할 수 있는 경우, 오브젝트 스토리지에서의 단순 덮어쓰기는 안전하지 않습니다.
세 번째 해결책: 버전 관리를 포함한 원자적 업데이트
최종적이고 안정적인 설계는 두 가지 아이디어를 결합합니다: 명시적인 버전 번호와 오브젝트의 ETag(스토리지 서비스의 체크섬 식별자)를 기반한 조건부 쓰기(conditional writes)입니다.
- 현재 체크포인트를 읽고(Read) ETag를 캡처합니다.
- 체크포인트 엔벨로프(envelope) 내부의 버전 필드를 증가(Increment) 시킵니다.
- 이전에 읽은 ETag와 일치할 경우에만 성공하는 조건부 요청을 사용하여 업데이트된 체크포인트를 작성(Write) 합니다.
- 다른 프로세스가 오브젝트를 변경하여 조건부 쓰기에 실패하면 전체 '읽기-증가-쓰기' 루프를 재시도(Retry) 합니다.
쓰기는 다른 프로세스가 파일을 변경하지 않았을 때만 성공하므로, 한 번에 하나의 워커만 새로운 상태를 커밋할 수 있습니다. 또한 버전 필드를 통해 오래된 체크포인트를 쉽게 감지하고 스키마가 변경될 때 이를 앞으로 마이그레이션할 수 있습니다.
이 패턴은 ETag 기반의 조건부 쓰기를 지원하는 오브젝트 스토리지에서 작동합니다.
AI 엔지니어를 위한 교훈
- SQLite는 프로토타입용으로만 사용하세요. 프로덕션 에이전트에는 스키마 변경과 동시 쓰기를 처리할 수 있는 저장소가 필요합니다.
- 스키마 마이그레이션을 직접 계획하세요. Typed dictionary는 정적 분석을 위한 형태를 설명하지만, 런타임 구조를 강제하지는 않습니다.
- 상태를 공유 리소스로 취급하세요. 동시성 버그는 조용한 데이터 손실로 나타나며, 이는 명시적인 예외보다 디버깅하기가 더 어렵습니다.
- 클라우드 프리미티브(primitives)를 활용하세요. ETag 기반의 조건부 쓰기는 별도의 잠금(lock) 서비스 없이도 저렴한 낙관적 잠금(optimistic locking)을 제공합니다.
- 모든 단계를 로그로 남기세요. LangGraph가 무시하는 누락된 필드와 같은 조용한 실패는 추적하기 가장 어렵습니다.
LangGraph 체크포인팅의 향후 전망은?
이미 동일한 난관에 부딪힌 팀들에게 원자적 업데이트 레시피는 빠르고 저렴한 해결책을 제공합니다. 이는 신뢰할 수 있는 프로덕션 파이프라인을 구축하는 데 무거운 상태 저장소가 필요한 것이 아니라, 동시성과 버전 관리를 세심하게 다루는 것이 중요하다는 것을 보여줍니다.
요약: 간단한 버전 관리 엔벨로프와 조건부 쓰기를 결합하면 불안정한 시스템을 신뢰할 수 있는 시스템으로 바꿀 수 있으며, AI 엔지니어가 끝없는 데이터 손실 디버깅 대신 에이전트 로직에 집중할 수 있게 해줍니다.
