LangGraph Checkpointer, Interrupt와 Memory
LangGraph의 State는 여러 Node가 같은 실행 과정에서 데이터를 공유하게 해 줍니다. 하지만 실제 agent는 한 번의 호출이 끝난 뒤에도 같은 대화를 이어 가고, 사람의 승인을 기다렸다가 나중에 다시 실행하거나, 장애가 발생했을 때 이전 상태에서 작업을 복구해야 할 수 있습니다. State의 구조를 정의하는 것만으로는 부족하고, 실행 상태를 어디에 얼마나 오래 보존할지도 결정해야 합니다.
LangGraph는 이 persistence 영역을 크게 Checkpointer와 Store로 나눕니다. Checkpointer는 특정 thread_id에 속한 graph state를 checkpoint로 저장해 같은 conversation의 상태를 다음 실행에서도 이어 갈 수 있게 합니다. 이 thread-scoped state가 short-term memory의 기반이 되고, human-in-the-loop, time travel, fault tolerance와 durable execution에도 사용됩니다. 다만 프로세스가 재시작된 뒤에도 상태를 복구하려면 메모리 기반이 아니라 database 등에 연결된 persistent checkpointer가 필요합니다.
Store는 범위가 다릅니다. Checkpointer가 하나의 thread에서 진행 중인 실행 상태를 보관한다면, Store는 특정 thread의 graph state 밖에서 여러 thread가 다시 사용할 데이터를 저장합니다. 사용자 선호, 장기적으로 기억해야 할 사실, 여러 conversation에서 공유할 지식 같은 long-term memory가 여기에 해당합니다.
interrupt()는 persistence 위에서 실행을 일시 정지시키는 primitive입니다. Node 안에서 interrupt()를 호출하면 graph state가 checkpointer에 저장되고 실행이 멈춥니다. 이후 같은 thread_id로 Command(resume=...)를 전달하면 저장된 실행을 다시 이어 갈 수 있습니다.
여기서 재개는 일반적인 프로그램의 breakpoint처럼 정확히 다음 명령어부터 실행되는 방식은 아닙니다. 해당 Node가 다시 처음부터 실행되고, interrupt()에 다시 도달하면 Command(resume=...)로 전달한 값이 그 호출의 반환값이 되어 이후 로직이 계속됩니다. 그래서 interrupt() 이전에 API 호출이나 파일 변경 같은 side effect가 있다면 재실행을 고려해 idempotent하게 설계해야 합니다.
Checkpointer가 graph state를 어떻게 thread 단위로 저장하는지부터 살펴보고, 그 위에서 interrupt()와 Command(resume=...)가 중단과 재개를 어떻게 만드는지 연결해서 봅니다. 이어서 Checkpointer의 thread-scoped short-term memory와 Store의 cross-thread long-term memory가 어디에서 갈리는지 정리합니다.
Checkpointer는 현재 thread의 실행 상태를 기억하고, Interrupt는 그 저장된 실행을 멈추고 다시 이어 가며, Store는 thread 밖에서도 다시 사용할 정보를 기억합니다.
1장. State와 persistence는 다른 문제다
State를 정의했다고 자동으로 다음 호출까지 값이 남는 것은 아닙니다.
class State(TypedDict):
messages: list
summary: str
이 스키마는 어떤 데이터를 공유할지를 정의할 뿐입니다. 실행이 끝난 뒤 값을 어디에 보관할지는 정하지 않습니다.
State schema
무엇을 저장할 수 있는가
Checkpointer
실행 중 State를 언제, 어디에 저장할 것인가
checkpointer를 연결하면 LangGraph는 그래프의 진행 과정에서 상태 스냅샷을 저장합니다. 공식 checkpoint 패키지는 이를 persistence layer로 설명하며, durable execution, interaction 사이의 memory, human-in-the-loop의 기반으로 사용합니다.
2장. 가장 작은 checkpointer 예제
개발과 테스트에서는 InMemorySaver로 시작할 수 있습니다.
from langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
checkpointer를 사용하면 실행할 때 thread_id를 함께 전달합니다.
config = {
"configurable": {
"thread_id": "conversation-1"
}
}
result = graph.invoke(inputs, config)
같은 thread_id로 다시 호출하면 checkpointer가 이전 checkpoint를 찾을 수 있습니다.
첫 호출
thread_id = conversation-1
↓
checkpoint 저장
두 번째 호출
thread_id = conversation-1
↓
이전 상태 조회 + 새 입력 반영
따라서 checkpointer를 붙이는 것과 thread_id를 설계하는 것은 같이 봐야 합니다.
3장. thread_id는 상태를 찾는 기본 키다
thread_id는 단순한 사용자 ID가 아닙니다. 어떤 실행들을 같은 상태 흐름으로 볼 것인지를 구분하는 키입니다.
대화형 애플리케이션에서는 보통 대화 하나를 하나의 thread로 보는 편이 자연스럽습니다.
user-42
├─ conversation-a → thread_id = conversation-a
└─ conversation-b → thread_id = conversation-b
사용자 ID 하나를 모든 대화의 thread_id로 사용하면 서로 다른 주제가 한 상태에 계속 쌓일 수 있습니다. 반대로 매 요청마다 새 ID를 만들면 이전 상태를 이어 갈 수 없습니다.
배치 워크플로처럼 각 실행이 완전히 독립적이라면 실행마다 새 ID를 사용할 수 있습니다.
import uuid
config = {
"configurable": {
"thread_id": str(uuid.uuid4())
}
}
판단 기준은 간단합니다. 두 실행이 같은 과거 상태를 이어서 봐야 하는가를 먼저 결정하면 됩니다.
4장. checkpoint에는 State의 한 시점이 저장된다
checkpoint는 단순히 메시지 목록 하나만 저장하는 장치가 아닙니다. 그래프가 관리하는 State의 스냅샷과 실행 메타데이터, pending writes 같은 정보를 함께 다룹니다.
예를 들어 State가 다음과 같다면:
class State(TypedDict):
messages: list
draft: str
score: float
approved: bool
checkpoint는 특정 시점의 messages, draft, score, approved를 함께 복원할 수 있는 기반이 됩니다.
이 점이 전통적인 "대화 메모리" 개념과 다른 부분입니다. 대화형 그래프에서는 messages가 가장 눈에 띄지만, 실제로 저장되는 대상은 그래프 State입니다.
그래서 사람 승인 상태나 중간 초안처럼 메시지가 아닌 값도 같은 실행 흐름에 포함할 수 있습니다.
LangGraph의 실행 메서드는 durability 수준도 조절할 수 있습니다. sync는 다음 단계로 넘어가기 전에 checkpoint 저장을 기다리고, async는 다음 단계와 저장을 겹쳐 지연을 줄이며, exit는 실행이 끝나거나 interrupt·오류로 빠져나갈 때 저장합니다. 모든 워크플로가 같은 내구성 비용을 지불할 필요는 없기 때문에 복구 요구와 성능 사이의 선택으로 봅니다.
5장. 메시지 이력이 이어지는 과정
MessagesState나 add_messages reducer와 checkpointer를 함께 쓰면 이전 메시지를 매 요청마다 애플리케이션이 직접 다시 조립하지 않아도 됩니다.
from langgraph.graph import MessagesState
class State(MessagesState):
pass
첫 번째 호출이 상태를 저장합니다.
graph.invoke(
{"messages": [("human", "내 이름은 홍길동이야")]},
config,
)
두 번째 호출에서는 새 메시지만 전달합니다.
graph.invoke(
{"messages": [("human", "내 이름이 뭐였지?")]},
config,
)
동작을 개념적으로 보면 다음과 같습니다.
thread_id로 이전 checkpoint 조회
↓
기존 messages 복원
↓
add_messages가 새 메시지 병합
↓
Node 실행
↓
새 State checkpoint 저장
여기서 역할이 나뉩니다. checkpointer는 State를 저장하고 복원하고, add_messages는 메시지 업데이트를 어떻게 병합할지 결정합니다.
6장. 저장했다고 컨텍스트가 자동으로 줄지는 않는다
checkpointer가 대화 상태를 이어 준다고 해서 모델의 컨텍스트 길이 문제까지 자동으로 해결되는 것은 아닙니다.
messages가 계속 누적되면 모델 호출에 전달되는 메시지도 계속 길어질 수 있습니다.
checkpoint
State를 보존
trimming / summarization
모델에 실제로 넣을 컨텍스트를 관리
따라서 장기 대화에서는 별도의 전략이 필요합니다. 최근 메시지만 선택하거나, 오래된 대화를 요약하거나, 장기 정보는 별도 store나 외부 메모리에 분리할 수 있습니다.
checkpointer를 "자동 요약 메모리"로 이해하지 않는 것이 중요합니다. 저장과 컨텍스트 압축은 다른 문제입니다.
7장. Interrupt는 저장된 실행을 멈추고 다시 잇는다
LangGraph는 실행 도중 interrupt()를 호출해 멈추고 외부 입력을 기다릴 수 있습니다. 예를 들어 위험한 작업 전에 사람 승인을 받는 흐름입니다.
from langgraph.types import interrupt
def review(state: State):
approved = interrupt("이 작업을 승인하시겠습니까?")
return {"approved": approved}
그래프는 checkpointer와 함께 컴파일합니다.
from langgraph.checkpoint.memory import InMemorySaver
graph = builder.compile(checkpointer=InMemorySaver())
실행이 interrupt에 도달하면 현재 State가 저장된 채로 멈춥니다. 나중에 같은 thread_id로 Command(resume=...)를 보내면 이어서 실행할 수 있습니다.
from langgraph.types import Command
graph.invoke(
Command(resume=True),
config,
)
중요한 것은 별도의 "재개 시스템"이 하나 더 생기는 것이 아니라는 점입니다. State가 checkpoint에 저장되어 있기 때문에 같은 실행을 이어 갈 수 있습니다.
interrupt()는 checkpointer 없이 사용할 수 없습니다. 또 재개할 때 Node의 interrupt() 다음 줄부터 단순히 이어지는 것이 아니라 해당 Node가 처음부터 다시 실행될 수 있으므로, interrupt 앞에 결제·메일 발송 같은 비가역 부작용을 두는 것은 피하거나 idempotent하게 설계해야 합니다.
8장. 과거 상태를 조회하고 다시 실행할 수 있다
checkpoint가 여러 시점에 쌓이면 현재 상태만 볼 수 있는 것이 아닙니다. 상태 이력을 조회해 과거 실행을 디버깅하거나 특정 시점에서 다시 실행할 수 있습니다.
이 기능은 일반적인 대화 기억보다 워크플로 디버깅에서 특히 유용합니다.
checkpoint 1 검색 완료
checkpoint 2 초안 생성
checkpoint 3 평가 실패
checkpoint 4 수정 완료
어느 시점에서 잘못된 값이 들어왔는지 확인할 수 있고, 과거 상태를 기준으로 실행 경로를 다시 검토할 수 있습니다.
다만 실제 운영에서 replay나 time travel을 사용할 때는 Node의 외부 부작용을 주의해야 합니다. 데이터베이스 쓰기, 결제, 이메일 전송처럼 다시 실행하면 중복 효과가 생기는 작업은 idempotency를 함께 설계해야 합니다.
9장. 저장소는 용도에 따라 고른다
개발 단계에서는 InMemorySaver가 가장 단순합니다. 하지만 프로세스가 종료되면 데이터가 사라지므로 테스트와 디버깅 용도에 맞습니다.
운영에서는 지속 가능한 checkpointer를 사용합니다.
| Saver | 용도 |
|---|---|
InMemorySaver |
테스트와 로컬 디버깅 |
SqliteSaver |
가벼운 데모나 소규모 로컬 저장 |
PostgresSaver |
운영 환경의 지속적인 checkpoint 저장 |
비동기 애플리케이션에서는 각 저장소의 async 구현을 사용해야 합니다.
운영 저장소를 고를 때는 단순히 "안 사라지는가"만 보면 부족합니다. 동시 실행, 보존 기간, 삭제 정책, 암호화, 민감 정보 저장 여부도 같이 봐야 합니다.
10장. Checkpointer와 Store는 서로 다른 메모리 범위를 맡는다
LangGraph 문서에서 memory를 이해할 때 가장 중요한 구분입니다. Checkpointer는 한 thread의 전체 그래프 State를 저장하고, Store는 여러 thread에서 접근할 애플리케이션 데이터를 따로 저장합니다.
Checkpointer
scope: thread_id
저장: 그래프 State의 checkpoint
용도: 대화 이어가기, interrupt/resume, fault tolerance, time travel
Store
scope: custom namespace
저장: 애플리케이션이 정의한 JSON/key-value 데이터
용도: 사용자 선호, 프로필, 장기 사실, 여러 대화가 공유할 정보
예를 들어 같은 사용자가 대화를 두 개 열었다고 해보겠습니다.
user-123
├─ conversation-a → checkpointer의 thread A
└─ conversation-b → checkpointer의 thread B
users/user-123/preferences
└─ Store에 저장된 장기 정보
두 대화는 서로 다른 checkpoint를 가지지만, 필요한 경우 같은 Store의 사용자 선호를 읽을 수 있습니다.
가장 작은 Store 예제는 다음과 같습니다.
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
store.put(
("users", "user-123"),
"preferences",
{"language": "ko", "answer_style": "concise"},
)
그래프를 컴파일할 때 checkpointer와 store를 함께 줄 수 있습니다.
graph = builder.compile(
checkpointer=checkpointer,
store=store,
)
따라서 "LangGraph memory = checkpoint"라고만 외우면 범위가 좁습니다. short-term/thread memory는 State + checkpointer, long-term/cross-thread memory는 Store라는 두 층으로 이해하는 편이 정확합니다.
11장. 정리
persistence를 이해할 때는 대화 이력 하나로 좁혀 보지 않는 것이 좋습니다. Checkpointer가 thread의 그래프 State를 단계별로 저장하면서 short-term memory와 재개 기능이 나오고, Store는 그 바깥에서 cross-thread long-term memory를 담당합니다.
State checkpoint
├─ 같은 thread의 상태 이어가기
├─ 대화 메시지 유지
├─ interrupt 후 재개
├─ 상태 이력 조회
└─ 과거 시점 기반 디버깅
그리고 thread_id는 checkpoint를 찾는 기본 키입니다. 어떤 요청들이 같은 실행 흐름을 공유할지 먼저 설계하고, 그 thread를 넘어 다시 써야 하는 정보만 Store로 분리해야 persistence 구조가 자연스럽게 유지됩니다.
다음 편에서는 직접 그린 StateGraph와 표준 에이전트 헬퍼의 경계를 봅니다. 도구 호출 루프를 매번 직접 만들 필요는 없지만, 도메인 고유의 흐름이 있을 때는 커스텀 그래프가 여전히 필요합니다.
참고자료
'AI Agent > Frameworks' 카테고리의 다른 글
| LangGraph (4) - 에이전트와 커스텀 그래프 (0) | 2024.10.10 |
|---|---|
| LiteLLM (1) - 전체 구조와 SDK (0) | 2024.08.29 |
| LangGraph (2) - State, Node, Edge (0) | 2024.08.05 |
| LangChain (4) - 조합하기 (0) | 2024.05.30 |
| LangGraph (1) - 왜 그래프인가 (0) | 2024.05.10 |
댓글