관심사
State machine: 가능한 상태·전이와 불변식
Stateful system: 저장·복구·동시성·중복 이벤트분류: Layer 12 - AI 시스템 & LLM 애플리케이션 | 선수지식: L10-20 (트리·그래프), L9-10 (설계 원칙), L12-60 (Agent)
FSM(Finite State Machine, 유한 상태 머신)은 시스템이 가질 수 있는 **유한한 상태(state)**와 상태를 바꾸는 이벤트(event), 허용된 **전이(transition)**를 명시하는 모델이다. 핵심은 정상 경로를 그림으로 예쁘게 표현하는 데 있지 않다. 현재 상태에서 허용되지 않은 이벤트를 거부해 불가능한 상태와 위험한 경로를 실행 전에 줄이는 것이 핵심이다.
Statechart(상태 차트)는 FSM에 계층 상태, 병렬 상태, history state(이력 상태) 같은 표현을 더한다. Statechart history는 이전에 활성화됐던 **중첩 하위 상태를 기억했다가 다시 진입할 때 복원하는 pseudo-state(의사 상태)**다. 이는 실행 중 발생한 event를 append-only 기록으로 보존해 workflow를 replay하는 **Temporal Event History(이벤트 이력)**와 다른 개념이다. Workflow graph(워크플로 그래프)는 실행 단계를 node(노드), 이동 조건을 edge(간선)로 나타내어 분기·반복·사람 승인을 실행 흐름 중심으로 읽게 한다. 셋은 경쟁 관계라기보다 같은 문제를 서로 다른 초점으로 표현한다.
상태 머신은 가능한 상태와 전이를 정의하는 모델이다. Stateful system(상태 보유 시스템)은 그 모델의 실행 상태를 실제 환경에서 저장·복원하고, 중복 이벤트·동시성·관측·버전 호환까지 책임지는 운영 시스템이다. 같은 전이 표를 사용해도 메모리에서만 실행하면 상태 머신일 수 있지만, crash 뒤 같은 실행을 이어 가려면 stateful system의 책임이 추가된다.
FSM: "지금 어떤 상태이며, 이 이벤트를 받아도 되는가?"Statechart: "상태가 중첩·병렬일 때도 규칙을 어떻게 유지하는가?"Workflow graph: "어떤 단계가 어떤 조건으로 다음 단계를 실행하는가?"초기 LLM(Large Language Model, 대규모 언어 모델) workflow는 보통 다음처럼 시작한다.
if (needsSearch) { const docs = await search(query); if (docs.length === 0) retry();}if (needsTool) { const result = await callTool(args);}두 단계일 때는 읽기 쉽다. 그러나 검색 실패, 권한 부족, 사람 승인 대기, 재시도 초과, 부분 성공, 취소, 서버 재시작이 붙으면 조건은 독립적으로 늘지 않고 서로 조합된다. 승인 여부는 DB row에, retry 횟수는 queue message에, 현재 화면은 UI에, 마지막 tool 결과는 prompt 문자열에 흩어질 수 있다. 그러면 다음 질문에 답하기 어려워진다.
APPROVE 이벤트가 유효한가?상태 머신의 철학은 분기 코드를 더 잘 정리하자가 아니라 가능한 상태와 전이를 먼저 닫고, 그 밖의 조합은 기본적으로 거부하자다. Agent가 다음 행동 일부를 고르더라도 코드가 허용한 node·edge·권한·종료 경계 밖으로 나갈 수 없게 만든다.
isDrafting, isWaitingApproval, isSending, isDone 같은 boolean 네 개는 이론상 16개 조합을 만든다. 실제로 허용하려는 실행 상태가 drafting | waiting_for_approval | sending | done | failed 다섯 개뿐이라면 나머지는 대부분 의미가 없거나 위험하다. 예를 들어 isWaitingApproval=true이면서 isDone=true인 조합이 저장되면, 승인 API와 완료 화면이 서로 다른 결론을 내릴 수 있다.
Tagged union(태그된 유니온)은 각 variant가 공통 discriminant field(판별 필드)를 가지고, 그 필드의 literal 값으로 서로 다른 데이터 형태를 구분하는 union type이다. 아래 SupportState에서는 value가 판별 필드이므로 상태별로 필요한 필드만 함께 둘 수 있고, 가능한 조합도 좁아진다.
type SupportState = | { value: "intake"; ticketRef: string } | { value: "drafting"; attempt: number; reviewFeedbackRef?: string } | { value: "retry_wait"; attempt: number; wakeAt: string } | { value: "waiting_for_approval"; draftRef: string; approval: "pending" } | { value: "sending"; draftRef: string; approvalEventId: string } | { value: "done"; ticketResultRef: string } | { value: "failed"; reason: "retry_exhausted" | "approval_timeout" | "cancelled"; };이 타입만으로 저장·중복 이벤트·외부 effect 문제가 모두 해결되지는 않는다. 다만 done인데 동시에 approval: "pending"인 값을 일반 코드 경로에서 만들기 어렵게 해, 불변식을 깨뜨릴 표면을 줄인다.
고객 문의를 읽고 답변 초안을 만든 뒤, 사람이 승인하면 ticket 답변을 발송하는 support-agent를 생각해 보자. 이 workflow에는 다음 상태와 이벤트가 있다.
State: intake | drafting | retry_wait | waiting_for_approval | sending | done | failedEvent: START | DRAFT_READY | TOOL_ERROR | RETRY | APPROVE | REJECT | TIMEOUT | SEND_OK | CANCELTransition: (state, event) -> next_state먼저 transition table(전이 표)을 적으면 정상 경로뿐 아니라 거부해야 할 조합이 보인다.
이 문서에서 attempt는 1부터 시작하는 현재 실행 번호이고, max_attempts는 최초 실행을 포함한 총 실행 상한이다. 따라서 max_attempts=3이면 1·2·3번 실행까지만 허용하며, 실패한 현재 실행의 attempt < max_attempts일 때만 다음 실행을 예약한다.
Guard(전이 가드)는 현재 state·event·context를 보고 특정 전이를 허용할지 판단하는 조건이다. Guard는 외부 시스템을 바꾸지 않고 같은 입력에 같은 결론을 내야 하며, 아래 표의 ticket reference가 있는가, attempt < max_attempts가 그 예다.
| 현재 상태 | 이벤트 | 다음 상태 | guard·처리 |
|---|---|---|---|
| 모든 후속 상태 | 이미 처리한 동일 event ID | 상태 유지 | 상태별 전이 검사보다 먼저 중복으로 판정하고 no-op |
intake | START | drafting | ticket reference가 있을 때만 허용 |
drafting | DRAFT_READY | waiting_for_approval | event payload에 draft_artifact_ref가 있어야 함 |
drafting | TOOL_ERROR | retry_wait | attempt < max_attempts |
drafting | TOOL_ERROR | failed | attempt >= max_attempts면 retry_exhausted |
retry_wait | RETRY | drafting | backoff timer가 끝났을 때 허용 |
waiting_for_approval | 새 APPROVE | sending | 처음 본 approval_event_id만 수락하고 effect용 멱등성 키를 생성 |
sending | 새 APPROVE | 거부 | 승인 대기 상태가 아니므로 invalid transition |
waiting_for_approval | REJECT | drafting | review_feedback_ref 필수, 새 tool 실행을 attempt=1로 시작 |
waiting_for_approval | TIMEOUT | failed | 승인 deadline이 지나면 approval_timeout |
waiting_for_approval | CANCEL | failed | 사용자 취소 |
sending | SEND_OK | done | 발송 결과 reference를 저장 |
done | 새 APPROVE | 거부 | 완료된 run은 새 승인으로 다시 진행하지 않음 |
APPROVE가 두 번 도착하는 반례가 중요하다. 승인 버튼의 network retry나 webhook 재전달 때문에 같은 이벤트가 중복될 수 있다. 첫 이벤트 approval_evt_91이 waiting_for_approval -> sending 전이를 만들고 발송 effect를 예약했다면, 두 번째 approval_evt_91은 현재 상태가 sending, done, failed 중 무엇이든 **이미 처리한 이벤트라서 no-op(아무 효과도 내지 않는 처리)**이어야 한다. 반대로 후속 상태에서 처음 보는 새 event ID의 APPROVE는 승인 대기 상태가 아니므로 invalid transition으로 거부한다. 따라서 검사 순서는 처리된 event ID 중복 확인 → 현재 상태에서 새 이벤트의 전이 유효성 확인이다.
여기서 approval_event_id는 들어온 승인 이벤트를 한 번만 소비하기 위한 event dedupe key이고, send:ticket_77:draft_v1은 외부 ticket 시스템에서 같은 발송을 반복하지 않기 위한 effect idempotency key다. 전자는 전이 중복을, 후자는 외부 변경 중복을 막으므로 하나의 키로 뭉개지 않는다.
한 실행을 손으로 따라가면 state, artifact, trace의 역할이 더 선명해진다. Durable state(내구 상태)는 재시작 뒤에도 실행을 이어 가기 위한 상태이고, artifact store는 큰 결과 원문의 정본을 보관하며, trace는 step과 latency를 관측용으로 연결한다.
| step | 이벤트·결정 | durable state의 변화 | 외부 정본·관측 연결 |
|---|---|---|---|
| 1 | START | intake -> drafting, attempt=1 | ticket_ref=ticket://77, trace_id=tr_204 |
| 2 | DRAFT_READY | event의 draft_artifact_ref를 받아 drafting -> waiting_for_approval | 초안 원문은 artifact store, checkpoint(재개 경계의 state snapshot)는 chk_0007 |
| 3 | APPROVE(evt_91) | approval_event_id=evt_91, waiting_for_approval -> sending | effect_idempotency_key=send:ticket_77:draft_v1 |
| 4 | 중복 APPROVE(evt_91) | sending 상태 유지 | 중복 이벤트를 trace에 표시하고 발송 effect는 생략 |
| 5 | SEND_OK | sending -> done, ticket_result_ref 저장 | 발송 결과 정본과 trace_id=tr_204를 연결 |
여기서 checkpoint가 초안 원문이나 모든 후보 생성 로그를 품을 필요는 없다. 재개에 필요한 현재 상태와 reference를 보존하고, 큰 원문과 완전한 결정 기록은 별도 정본에서 읽는다.
전이 판단을 순수 함수에 가깝게 두면 상태·이벤트 조합을 표 기반으로 테스트할 수 있다.
type ApprovalEvent = { type: "APPROVE"; eventId: string; reviewerId: string;};
function transition( state: SupportState, event: ApprovalEvent, processedEventIds: ReadonlySet<string>,): SupportState { if (processedEventIds.has(event.eventId)) return state;
if (state.value !== "waiting_for_approval") { throw new Error(`APPROVE is invalid from ${state.value}`); }
return { value: "sending", draftRef: state.draftRef, approvalEventId: event.eventId, };}이 함수는 이메일이나 ticket API를 호출하지 않는다. processedEventIds가 같은 승인 이벤트를 no-op으로 만들지만, 원자적 저장(atomic persistence), 즉 처리한 event ID와 새 state를 한 commit 경계에서 모두 반영하거나 둘 다 반영하지 않는 계약은 stateful system의 책임이다. 실제 발송은 effect executor가 담당한다. 따라서 코드 한 조각만 보고 exactly-once 실행이 보장된다고 오해하면 안 된다.
평면 FSM의 상태를 계속 늘리면 drafting_compliance_pending, drafting_compliance_passed, revising_compliance_pending처럼 조합 상태가 폭발한다. Statechart는 공통 규칙을 상위 상태에 모으고, 독립 진행을 병렬 영역으로 분리한다.
running.drafting, running.calling_toolanswer_drafting과 compliance_check를 동시에 진행confidence >= 0.8이고 정책 검사를 통과할 때만 자동 진행Statechart의 history는 “이전에 어느 하위 상태에 있었는가”를 표현하는 모델 요소다. Temporal의 event-history replay 의미론과 같은 뜻이 아니다.
support-agent에서 초안 작성과 compliance 검사가 독립이라면 상태 이름을 조합하는 대신 병렬 영역으로 둘 수 있다.
running (parallel) answer: drafting -> ready compliance: checking -> passed | rejected두 영역이 ready + passed일 때만 -> waiting_for_approval이렇게 하면 drafting_compliance_checking, ready_compliance_passed 같은 조합 상태를 일일이 추가하지 않고도 합류 guard를 명시할 수 있다.
Workflow graph는 node가 실행 단위를, edge가 이동 조건을 나타낸다.
START -> classify -> retrieve -> draft -> judge ├─ pass -> wait_for_approval -> send -> final ├─ fail && attempt < 3 -> revise -> judge └─ fail && attempt >= 3 -> human_escalationDAG(Directed Acyclic Graph, 방향 비순환 그래프)는 cycle이 없어서 위상 순서로 실행할 수 있다. 반면 agent workflow graph는 revise -> judge처럼 의도적인 cycle을 가질 수 있다. 따라서 graph라는 이유만으로 종료가 보장되지는 않는다. 반복 edge에는 attempt < 3, budget, deadline, fallback 같은 종료 불변식이 필요하다.
FSM은 상태와 이벤트의 유효성을 검토하기 좋고, graph는 step 간 데이터 흐름과 반복 경로를 읽기 좋다. 실무 구현은 graph node를 실행하면서 shared state의 FSM 불변식을 검사하는 식으로 둘을 함께 쓰기도 한다.
먼저 아래 분류는 특정 라이브러리 API가 아니라 제품·워크플로 설계의 책임을 나누는 용어다.
| 제품 중립 분류 | 역할 | support-agent 예시 |
|---|---|---|
| Transition guard(전이 가드) | 전이 가능 여부를 순수하게 판단 | draft가 있고 승인 이벤트가 아직 처리되지 않았는가 |
| Internal update(내부 갱신) | workflow가 소유한 state를 갱신 | approval_status="approved", attempt += 1 |
| External effect(외부 효과) | workflow 밖의 시스템을 바꿈 | ticket 답변 발송, 이메일 전송, 외부 DB 업데이트 |
이 분류의 Internal update와 External effect를 XState의 action과 같은 뜻으로 읽으면 안 된다. XState에서 action은 전이에 반응해 실행되는 fire-and-forget effect(실행을 시작한 뒤 완료나 결과를 기다리지 않는 효과)라는 프레임워크 용어이며, assign(...)처럼 context를 갱신하는 built-in action뿐 아니라 logging이나 다른 actor로 event를 보내는 action도 포함한다. 즉 XState의 actions 배열에는 위의 내부 갱신과 외부 효과에 해당하는 구현이 모두 들어갈 수 있다. 이 문서에서는 설계 책임을 논할 때 제품 중립 용어를 쓰고, 실제 XState 설정을 가리킬 때만 XState action이라고 부른다. 비동기 API 호출이나 이메일 발송의 완료·실패가 다음 전이를 결정해야 한다면 async action을 기다리게 만들지 말고, Promise actor를 invoke한 뒤 onDone·onError 전이로 모델링한다.
Guard 안에서 외부 API를 호출하면 같은 전이 평가가 서로 다른 결과를 내거나 외부 효과를 몰래 발생시킬 수 있다. 내부 갱신과 외부 효과를 하나의 불투명한 작업으로 섞으면 checkpoint 복원 뒤 어느 부분까지 성공했는지 판별하기 어렵다. 결정은 재평가 가능하게, 외부 효과는 식별·중복 제거 가능하게 분리하는 것이 핵심이다.
LLM이 graph node에서 다음 행동을 고를 수는 있지만, 자유 텍스트를 곧바로 edge condition으로 사용하면 parsing 실패와 권한 우회가 workflow 실패로 번진다. Structured output(구조화 출력) 계약을 두고 애플리케이션이 schema와 허용 전이를 다시 검증한다.
{ "decision": "call_tool | ask_human | final", "confidence": 0.74, "reason": "환불 정책 근거가 한 건 부족함", "tool_args": { "tool": "search_policy", "query": "enterprise refund window" }}confidence=0.74라는 숫자만으로 자동 승인해서는 안 된다. confidence가 calibration(예측 신뢰도와 실제 정답률이 맞도록 검증·보정하는 과정)되지 않았거나 입력 분포가 바뀌면 0.8 threshold도 의미가 달라진다. decision enum, tool allowlist(호출을 미리 허용한 도구 목록), 인자 schema, 현재 상태의 guard를 deterministic code(같은 입력에 같은 판단을 내리는 코드)가 검사하고, 고위험 effect는 사람 승인 상태를 별도로 요구한다.
재시도를 같은 edge의 숨은 반복으로 만들면 현재 시도 횟수와 대기 이유가 사라진다. 상태로 올리면 retry budget과 영구 실패 경계를 관측할 수 있다.
acting -> failed_transient -> retry_wait -> acting -> failed_permanent현재 실행이 실패했을 때 attempt < max_attempts는 다음 실행을 허용하는 transition guard이고, 허용되면 attempt += 1과 다음 시각인 wakeAt을 workflow state에 기록한다. 예를 들어 max_attempts=3에서 1번 실패 후 2번, 2번 실패 후 3번 실행을 예약하지만, 3번 실패 후에는 재시도하지 않고 retry_exhausted로 전이한다. Workflow runtime이 wakeAt까지 실행을 내구성 있게 재개하는 durable timer는 런타임의 persistence·scheduling 책임이다. 애플리케이션이 별도 cron·queue 같은 외부 scheduler에 작업을 등록하는 경우에만 그 등록을 external effect로 보고 idempotency key를 둔다. 이미 외부 시스템을 바꾼 뒤 다음 단계가 실패했다면 단순 retry로 원상 복구되지 않는다. 이때 Saga의 compensation(보상 작업)처럼 cancel_ticket_update, revoke_credit 같은 별도 node를 두고, 보상 자체도 실패·재시도할 수 있음을 상태에 반영한다.
앞에서 정의했듯 상태 머신은 어떤 상태에서 어떤 이벤트를 허용할지 정의하는 모델이고, stateful system은 그 실행 상태의 저장·복원·중복 제거·관측·호환 전략까지 포함한다.
State machine: 가능한 상태·전이와 불변식
Stateful system: 저장·복구·동시성·중복 이벤트State machine: 상태도와 transition table
Stateful system: checkpoint, event/artifact reference, migrationState machine: invalid transition 거부
Stateful system: crash, stale write, version 충돌 복구State machine: state × event 조합
Stateful system: restart, duplicate delivery, old run 재개예를 들어 waiting_for_approval + APPROVE -> sending은 상태 머신 설계다. 승인 이벤트와 checkpoint를 일관되게 기록하고, 같은 이벤트가 두 번 와도 발송 effect가 한 번만 예약되게 하며, 배포 뒤 기존 실행에 최신 규칙을 호환 적용할지 구버전 경로로 routing할지 정하는 것은 stateful system 설계다.
이 문서에서 LangGraph checkpoint(체크포인트)는 thread의 graph state를 각 super-step 경계에 저장한 StateSnapshot이다. Super-step은 그 시점에 예약된 하나 이상의 node가 병렬로 실행될 수 있는 graph의 한 번의 진행 단위다. Snapshot에는 state channel 값, 다음에 실행할 node, step metadata와 task 정보 등이 들어가며, 재개와 분기의 기준이 된다.
Checkpoint는 외부 effect가 원자적으로 끝났음을 보장하는 “안전 지점”이나 트랜잭션 경계가 아니다. 한 super-step 안에서 외부 API 호출은 성공했지만 node 완료 전 process가 죽을 수 있으므로, 외부 effect의 idempotency key와 결과 reference는 별도 계약으로 남겨야 한다. 여기서 정본은 사후에도 완전한 사실을 다시 읽을 수 있는 기준 기록을 뜻한다. Checkpoint는 모든 원문과 로그를 복제하는 저장소가 아니라 다음 전이와 재개에 꼭 필요한 상태 및 정본 reference를 가진다.
Artifact 원문을 저장하거나 checkpoint에 reference를 기록하기 전에는 payload뿐 아니라 reference 경로와 metadata에도 PII(개인 식별 정보)·secret이 포함됐는지 분류하고 redaction한다. Redaction하지 않은 원문이 재현에 꼭 필요하다면 접근 통제·암호화된 정본에만 두고, checkpoint에는 사용자 이메일이나 token을 드러내지 않는 opaque reference만 저장한다.
| 기록 | 보존할 내용 | 보존하지 않을 내용 |
|---|---|---|
| durable state/checkpoint | state channel 값, next node/task, attempts, 승인 상태, resume-critical 결정·결과 reference, artifact reference, trace_id | 전체 prompt, 전체 후보·탈락 로그, 큰 tool 결과 원문 |
| artifact/event store | 초안·tool 결과 원문, 전체 candidate/drop ID·score·reason, effect event | 호출별로 렌더링된 중복 bundle |
| observability trace | step·latency·version, checkpoint/artifact reference, 진단 요약 | 장기 정본이 필요한 전체 기록 |
| context bundle | 현재 LLM 호출에 필요한 state·정본의 projection(투영본) | 다음 호출까지 보존해야 할 source of truth |
Context Engineering과의 계약은 짧게 정리할 수 있다. Durable state는 목표·현재 단계와 resume-critical 결정·결과 reference, artifact reference, trace_id를 제공하고, context pipeline은 이를 versioned source와 결합해 호출별 bundle을 렌더링한다. Bundle은 projection이므로 checkpoint를 대체하지 않는다. 전체 candidate/drop 로그의 정본도 checkpoint나 sampling되는 trace가 아니라 durable artifact 또는 append-only event store에 둔다.
support-agent checkpoint는 다음 정도로 시작할 수 있다.
{ "run_id": "run_20260522_001", "graph_version": "support-agent-v1", "state_schema_version": 2, "checkpoint_id": "chk_0008", "current_node": "sending", "state": { "attempt": 1, "approval_status": "approved", "decision_refs": ["decision://draft-policy/77/v1"], "result_refs": ["artifact://drafts/77/v1"], "artifact_refs": [ "artifact://context-decisions/run_20260522_001/retrieve/v1" ] }, "last_event_id": "approval_evt_91", "processed_event_ledger_ref": "event://support-agent/run_20260522_001/approvals", "effect_idempotency_key": "send:ticket_77:draft_v1", "trace_id": "tr_204", "updated_at": "2026-05-22T09:30:00Z"}이 schema는 제품별 정답이 아니라 책임 경계를 보는 최소 예시다. State가 커질수록 “다음 전이에 꼭 필요한가, 아니면 reference로 충분한가”를 먼저 묻는다.
LangGraph checkpoint는 graph state를 snapshot에서 재개하는 기능이지, 외부 effect의 exactly-once 실행, Temporal의 deterministic event-history replay, 과거 graph code로의 자동 version pinning을 보장하지 않는다. 이 문서의 핵심 소유 범위는 checkpoint에 둘 resume-critical state와 정본 reference, 전이 불변식, effect 멱등성 경계까지다.
수시간에서 수일 동안 실행되는 workflow의 durable replay에서는 Event History, workflow code 결정성, 기록된 Activity 결과 복원, Worker Versioning이 별도 계약이 된다. 그 상세 메커니즘은 다음 문서인 content/topics/L12/temporal-durable-execution-ai-workflows.mdx로 넘긴다. 아래 선택 심화는 두 제품의 경계를 비교하거나 기존 LangGraph thread를 배포 뒤에도 이어 가야 할 때만 읽으면 된다.
1. Pending writes가 재사용하는 범위
Checkpointer를 사용한 같은 super-step에서 일부 node만 실패했다면, 이미 성공한 다른 node의 출력은 task 단위 pending writes로 저장되어 재개 시 재사용되고 실패한 node만 다시 실행될 수 있다. 이는 성공 node의 state write를 재계산하지 않는 보장이지, 그 node가 호출한 외부 API의 exactly-once 보장은 아니다.
2. LangGraph checkpoint replay와 Temporal history replay의 차이
replay라는 단어가 같아도 복원 단위와 계약이 다르다.
| 관점 | LangGraph checkpoint 기반 재개·replay | Temporal deterministic event-history replay |
|---|---|---|
| 기준 기록 | super-step 경계의 thread StateSnapshot | append-only workflow event history |
| 주된 목적 | 특정 graph state에서 재개·분기하고 이후 node를 다시 실행 | workflow code가 과거 history와 같은 결정을 내리도록 실행 상태 재구성 |
| 외부 결과 취급 | graph state/checkpoint와 node의 멱등성 정책에 의존 | 기록된 activity result를 history에서 읽고 workflow 결정을 재현 |
| 이 문서가 소유할 범위 | checkpoint에 필요한 graph state·reference와 전이 불변식 | 연결 개념만 제시하고 상세 replay 의미론은 다음 Temporal 문서로 넘김 |
따라서 “checkpoint가 있으니 Temporal과 같은 deterministic replay가 된다”고 결론 내리면 안 된다. 이 문서는 graph state를 어느 super-step snapshot에서 복원하고 어떤 전이를 허용할지까지 다룬다. Temporal의 history event 종류, workflow code 결정성, patch/version marker 의미론은 후속 L12-120 문서의 범위다.
예를 들어 LangGraph에서 waiting_for_approval checkpoint를 골라 재개하면 이후 send node가 다시 실행 대상이 될 수 있으므로 node와 effect의 멱등성이 필요하다. Temporal이 과거 history를 replay해 workflow 상태를 재구성할 때는 이미 완료된 activity 결과를 history에서 읽으며 그 activity 자체를 replay 과정에서 다시 호출하지 않는다. 같은 “다시 이어 달리기”처럼 보여도 보장 근거가 다르다.
3. Versioning — LangGraph는 기존 thread에도 최신 graph를 적용한다
LangGraph는 실행을 시작한 코드 버전에 thread를 자동 고정하지 않는다. 새 graph를 배포하면 새 thread뿐 아니라 checkpoint에서 재개하는 기존 thread도 최신 배포 graph로 실행한다. 따라서 graph_version 필드를 checkpoint에 기록하는 것만으로 v1 코드가 선택되지는 않는다. 이 필드는 애플리케이션이 호환 분기나 routing에 사용할 수 있는 metadata일 뿐이다.
support-agent-v1의 waiting_for_approval thread가 남아 있는데 최신 graph가 needs_legal_review node를 추가했다고 하자. 기본 전제는 기존 checkpoint를 최신 graph가 읽는 것이다. 안전한 변경은 다음 세 층으로 나눈다.
flow_version=2를 기록하고 conditional edge가 기존 thread의 v1 default와 새 thread의 v2 경로를 나눈다.thread_id/version metadata로 v1·v2 graph deployment를 routing하고, v1 thread가 끝날 때까지 구버전 worker를 유지한 뒤 drain한다. 또는 명시적 state migration 후 최신 graph로 합류시킨다.즉 “진행 중 run은 v1 graph로 재개한다”는 LangGraph의 기본 보장이 아니라 애플리케이션의 version routing·drain·compatibility 전략이다. 최신 graph가 기존 checkpoint를 읽을 때 새 guard가 기대하는 필드가 없거나, 이미 승인된 thread가 다시 법무 검토로 들어가지 않도록 호환 경로를 먼저 설계해야 한다. Migration 중에도 기존 approval_event_id와 external effect의 idempotency key를 보존해야 중복 승인이 새 발송으로 해석되지 않는다.
| 상황 | 우선 모델 | 이유 |
|---|---|---|
| 상태가 적고 이벤트 유효성 검사가 핵심 | FSM | transition table과 state × event 테스트가 단순 |
| 중첩·병렬 상태가 조합 폭발을 만든다 | Statechart | 상위 규칙과 병렬 영역으로 중복을 줄임 |
| 여러 실행 step, 조건 분기, 반복 경로를 읽어야 한다 | Workflow graph | node·edge·shared state로 흐름이 보임 |
| 수시간~수일 대기, timer, crash 복구가 핵심 | Durable workflow engine 검토 | 저장·retry·signal·history가 별도 운영 책임이 됨 |
| 두세 단계 선형 처리이고 실패 시 처음부터 재실행 가능 | 일반 함수·job으로 시작 | 상태 머신의 추가 모델링 비용이 이득보다 큼 |
상태 수가 많다는 사실만으로 statechart가 필요한 것은 아니다. 상태가 20개여도 서로 독립 조합되지 않고 전이 표가 명확하면 평면 FSM이 더 읽기 쉬울 수 있다. 반대로 상태가 6개뿐이어도 승인과 compliance가 병렬이고 각 영역이 독립적으로 변하면 statechart가 유리하다.
| 관측 증상 | 먼저 의심할 설계 결함 | 확인할 불변식 |
|---|---|---|
| 같은 tool을 같은 인자로 계속 호출 | unbounded loop, 종료 guard 누락 | attempt·budget·deadline 상한이 있는가 |
| 승인 버튼 한 번에 이메일이 두 번 발송 | 이벤트 dedupe와 effect 멱등성 누락 | event ID와 effect idempotency key가 분리돼 있는가 |
| 완료된 run이 승인 이벤트로 다시 움직임 | terminal state의 invalid transition 허용 | done에서 나가는 edge가 닫혀 있는가 |
| 재시작 뒤 이미 성공한 node를 무조건 재실행 | super-step 경계 또는 effect/update 분리 실패 | 최근 snapshot과 외부 결과 reference가 연결됐는가 |
| prompt와 checkpoint 크기가 step마다 증가 | 원문·후보 로그를 state에 누적 | state에는 resume-critical reference만 남는가 |
| trace는 있는데 사고 당시 근거를 못 찾음 | trace를 정본처럼 사용 | durable artifact reference가 trace에 연결돼 있는가 |
| 배포 뒤 오래된 run만 실패 | 최신 graph와 기존 checkpoint의 호환성 누락 | routing·호환 분기·drain·migration 중 전략이 있는가 |
| 승인 후 엉뚱한 단계로 복귀 | history state 또는 resume target 모델링 오류 | 승인 전 checkpoint와 허용 전이가 일치하는가 |
| 같은 입력의 전이 테스트가 간헐적으로 다름 | guard 안의 시간·랜덤·외부 API 같은 side effect | guard가 순수하고 deterministic한가 |
| 모든 node 수정이 하나의 거대 state에 번짐 | everything node, hidden state, 낮은 응집도 | node가 하나의 결정 단위를 소유하는가 |
실패 신호는 바로 제품 설정을 바꾸라는 처방이 아니다. 먼저 run_id, checkpoint_id, graph_version, last_event_id, artifact_ref, trace_id를 연결해 어느 불변식이 처음 깨졌는지 좁힌다. 그 뒤 transition 결함인지, persistence 경쟁 조건인지, context projection 누락인지, effect 중복인지 소유 계층을 나눈다.
retry_wait를 제외한 안정 상태 6개 이하로 machine definition을 만든다.retry_wait와 retry 소진 전이를 추가해 숨은 반복이 드러나는지 본다.state × event 표에서 허용 전이와 거부 전이를 각각 테스트한다.APPROVE를 두 번 보내 두 번째 이벤트가 발송 effect를 만들지 않는지 확인한다.XState action이라는 API 이름과 별개로 guard·internal update·external effect의 설계 책임이 분리됐는지 검토한다.judge -> revise loop에 총 실행 상한 max_attempts=3과 fallback node를 두고, attempt=1부터 시작해 실행이 세 번을 넘지 않는지 확인한다.StateSnapshot에서 next node/task, attempt, artifact reference, trace_id가 이어지는지 확인한다.StateSnapshot이며, 외부 effect의 완료나 원자성을 보장하지 않는다.최종 수정: 2026-07-11