콘텐츠로 이동

상태 머신과 워크플로 그래프 설계

분류: Layer 12 - AI 시스템 & LLM 애플리케이션 | 선수지식: L10-20 (트리·그래프), L9-10 (설계 원칙), L12-60 (Agent)

상태 머신과 워크플로 그래프 설계 — FSM, Statecharts, XState, LangGraph

섹션 제목: “상태 머신과 워크플로 그래프 설계 — FSM, Statecharts, XState, LangGraph”

1. 정의 — 가능한 상태와 전이를 먼저 닫는다

섹션 제목: “1. 정의 — 가능한 상태와 전이를 먼저 닫는다”

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: "어떤 단계가 어떤 조건으로 다음 단계를 실행하는가?"

2. 선행 한계 → 등장 — if/else 흐름은 실행 상태를 숨긴다

섹션 제목: “2. 선행 한계 → 등장 — if/else 흐름은 실행 상태를 숨긴다”

초기 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 이벤트가 유효한가?
  • 이미 성공한 external effect(외부 효과, workflow 밖의 ticket·이메일·DB를 바꾸는 작업)를 재시도해도 되는가?
  • 재시작 후 무엇만 복원하면 다음 결정을 이어 갈 수 있는가?

상태 머신의 철학은 분기 코드를 더 잘 정리하자가 아니라 가능한 상태와 전이를 먼저 닫고, 그 밖의 조합은 기본적으로 거부하자다. Agent가 다음 행동 일부를 고르더라도 코드가 허용한 node·edge·권한·종료 경계 밖으로 나갈 수 없게 만든다.

3.1 상태는 값의 묶음이 아니라 불변식이다

섹션 제목: “3.1 상태는 값의 묶음이 아니라 불변식이다”

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"인 값을 일반 코드 경로에서 만들기 어렵게 해, 불변식을 깨뜨릴 표면을 줄인다.

3.2 Worked example — 고객 문의 support-agent

섹션 제목: “3.2 Worked example — 고객 문의 support-agent”

고객 문의를 읽고 답변 초안을 만든 뒤, 사람이 승인하면 ticket 답변을 발송하는 support-agent를 생각해 보자. 이 workflow에는 다음 상태와 이벤트가 있다.

State: intake | drafting | retry_wait | waiting_for_approval | sending | done | failed
Event: START | DRAFT_READY | TOOL_ERROR | RETRY | APPROVE | REJECT | TIMEOUT | SEND_OK | CANCEL
Transition: (state, event) -> next_state

먼저 transition table(전이 표)을 적으면 정상 경로뿐 아니라 거부해야 할 조합이 보인다.

이 문서에서 attempt1부터 시작하는 현재 실행 번호이고, 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
intakeSTARTdraftingticket reference가 있을 때만 허용
draftingDRAFT_READYwaiting_for_approvalevent payload에 draft_artifact_ref가 있어야 함
draftingTOOL_ERRORretry_waitattempt < max_attempts
draftingTOOL_ERRORfailedattempt >= max_attemptsretry_exhausted
retry_waitRETRYdraftingbackoff timer가 끝났을 때 허용
waiting_for_approvalAPPROVEsending처음 본 approval_event_id만 수락하고 effect용 멱등성 키를 생성
sendingAPPROVE거부승인 대기 상태가 아니므로 invalid transition
waiting_for_approvalREJECTdraftingreview_feedback_ref 필수, 새 tool 실행을 attempt=1로 시작
waiting_for_approvalTIMEOUTfailed승인 deadline이 지나면 approval_timeout
waiting_for_approvalCANCELfailed사용자 취소
sendingSEND_OKdone발송 결과 reference를 저장
doneAPPROVE거부완료된 run은 새 승인으로 다시 진행하지 않음

APPROVE가 두 번 도착하는 반례가 중요하다. 승인 버튼의 network retry나 webhook 재전달 때문에 같은 이벤트가 중복될 수 있다. 첫 이벤트 approval_evt_91waiting_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의 변화외부 정본·관측 연결
1STARTintake -> drafting, attempt=1ticket_ref=ticket://77, trace_id=tr_204
2DRAFT_READYevent의 draft_artifact_ref를 받아 drafting -> waiting_for_approval초안 원문은 artifact store, checkpoint(재개 경계의 state snapshot)는 chk_0007
3APPROVE(evt_91)approval_event_id=evt_91, waiting_for_approval -> sendingeffect_idempotency_key=send:ticket_77:draft_v1
4중복 APPROVE(evt_91)sending 상태 유지중복 이벤트를 trace에 표시하고 발송 effect는 생략
5SEND_OKsending -> 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 실행이 보장된다고 오해하면 안 된다.

3.4 Statechart — 상태가 중첩되거나 병렬일 때

섹션 제목: “3.4 Statechart — 상태가 중첩되거나 병렬일 때”

평면 FSM의 상태를 계속 늘리면 drafting_compliance_pending, drafting_compliance_passed, revising_compliance_pending처럼 조합 상태가 폭발한다. Statechart는 공통 규칙을 상위 상태에 모으고, 독립 진행을 병렬 영역으로 분리한다.

  • Hierarchical state(계층 상태): running.drafting, running.calling_tool
  • Parallel state(병렬 상태): answer_draftingcompliance_check를 동시에 진행
  • History state(이력 상태): 사람 검토 후 중단 전 하위 상태로 복귀
  • Guard(가드): 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를 명시할 수 있다.

3.5 Workflow graph — 실행 단계와 반복을 읽는다

섹션 제목: “3.5 Workflow graph — 실행 단계와 반복을 읽는다”

Workflow graph는 node가 실행 단위를, edge가 이동 조건을 나타낸다.

START
-> classify
-> retrieve
-> draft
-> judge
├─ pass -> wait_for_approval -> send -> final
├─ fail && attempt < 3 -> revise -> judge
└─ fail && attempt >= 3 -> human_escalation

DAG(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 불변식을 검사하는 식으로 둘을 함께 쓰기도 한다.

3.6 Guard, Internal update, External effect를 분리한다

섹션 제목: “3.6 Guard, Internal update, External effect를 분리한다”

먼저 아래 분류는 특정 라이브러리 API가 아니라 제품·워크플로 설계의 책임을 나누는 용어다.

제품 중립 분류역할support-agent 예시
Transition guard(전이 가드)전이 가능 여부를 순수하게 판단draft가 있고 승인 이벤트가 아직 처리되지 않았는가
Internal update(내부 갱신)workflow가 소유한 state를 갱신approval_status="approved", attempt += 1
External effect(외부 효과)workflow 밖의 시스템을 바꿈ticket 답변 발송, 이메일 전송, 외부 DB 업데이트

이 분류의 Internal updateExternal 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 복원 뒤 어느 부분까지 성공했는지 판별하기 어렵다. 결정은 재평가 가능하게, 외부 효과는 식별·중복 제거 가능하게 분리하는 것이 핵심이다.

3.7 LLM node는 구조화된 제안만 반환한다

섹션 제목: “3.7 LLM node는 구조화된 제안만 반환한다”

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는 사람 승인 상태를 별도로 요구한다.

3.8 Retry와 Compensation도 상태로 보인다

섹션 제목: “3.8 Retry와 Compensation도 상태로 보인다”

재시도를 같은 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를 두고, 보상 자체도 실패·재시도할 수 있음을 상태에 반영한다.

4. Checkpoint와 durable state — 재개에 필요한 것만 보존한다

섹션 제목: “4. Checkpoint와 durable state — 재개에 필요한 것만 보존한다”

4.1 State machine과 stateful system의 경계

섹션 제목: “4.1 State machine과 stateful system의 경계”

앞에서 정의했듯 상태 머신은 어떤 상태에서 어떤 이벤트를 허용할지 정의하는 모델이고, stateful system은 그 실행 상태의 저장·복원·중복 제거·관측·호환 전략까지 포함한다.

State machine과 stateful system 비교

관심사

State machine: 가능한 상태·전이와 불변식

Stateful system: 저장·복구·동시성·중복 이벤트

산출물

State machine: 상태도와 transition table

Stateful system: checkpoint, event/artifact reference, migration

실패 처리

State 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 설계다.

4.2 Checkpoint, artifact, trace, context bundle의 소유권

섹션 제목: “4.2 Checkpoint, artifact, trace, context bundle의 소유권”

이 문서에서 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/checkpointstate 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 tracestep·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로 충분한가”를 먼저 묻는다.

4.3 Checkpoint가 소유하지 않는 보장

섹션 제목: “4.3 Checkpoint가 소유하지 않는 보장”

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를 배포 뒤에도 이어 가야 할 때만 읽으면 된다.

선택 심화 — LangGraph pending writes·replay·최신 graph 호환 전략

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 기반 재개·replayTemporal deterministic event-history replay
기준 기록super-step 경계의 thread StateSnapshotappend-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-v1waiting_for_approval thread가 남아 있는데 최신 graph가 needs_legal_review node를 추가했다고 하자. 기본 전제는 기존 checkpoint를 최신 graph가 읽는 것이다. 안전한 변경은 다음 세 층으로 나눈다.

  1. 기술 호환성: 기존 state key와 type을 읽을 수 있게 새 필드는 optional/default로 추가하고, interrupted thread가 진입할 수 있는 node는 drain(새 실행을 받지 않고 진행 중인 실행만 끝내는 단계적 종료) 전까지 rename·remove하지 않는다.
  2. 업무 규칙 호환성: 새 thread 시작 때 flow_version=2를 기록하고 conditional edge가 기존 thread의 v1 default와 새 thread의 v2 경로를 나눈다.
  3. 구버전 고정이 필요한 배포: 애플리케이션이나 배포 계층이 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를 보존해야 중복 승인이 새 발송으로 해석되지 않는다.

상황우선 모델이유
상태가 적고 이벤트 유효성 검사가 핵심FSMtransition table과 state × event 테스트가 단순
중첩·병렬 상태가 조합 폭발을 만든다Statechart상위 규칙과 병렬 영역으로 중복을 줄임
여러 실행 step, 조건 분기, 반복 경로를 읽어야 한다Workflow graphnode·edge·shared state로 흐름이 보임
수시간~수일 대기, timer, crash 복구가 핵심Durable workflow engine 검토저장·retry·signal·history가 별도 운영 책임이 됨
두세 단계 선형 처리이고 실패 시 처음부터 재실행 가능일반 함수·job으로 시작상태 머신의 추가 모델링 비용이 이득보다 큼

상태 수가 많다는 사실만으로 statechart가 필요한 것은 아니다. 상태가 20개여도 서로 독립 조합되지 않고 전이 표가 명확하면 평면 FSM이 더 읽기 쉬울 수 있다. 반대로 상태가 6개뿐이어도 승인과 compliance가 병렬이고 각 영역이 독립적으로 변하면 statechart가 유리하다.

  • Graph를 그리면 안전하다: 종료 guard, effect 멱등성, state schema가 없으면 그림만 명시적일 뿐 실행은 여전히 위험하다.
  • LLM judge는 guard다: judge 출력은 확률적 제안이다. Guard는 그 출력을 schema·권한·현재 상태에 비추어 허용할지 결정하는 코드다.
  • Checkpoint는 전체 로그 백업이다: 재개에 필요한 state와 reference가 핵심이다. 큰 원문과 완전한 선택 기록은 별도 정본에 둔다.
  • 모든 workflow에 engine이 필요하다: 짧고 재실행 가능한 선형 작업은 함수와 queue job이 더 단순하다.
  • 평가 데이터까지 이 문서가 정의한다: 전이 테스트와 회귀 fixture는 다루지만, gold dataset의 표준어와 품질 평가 계약은 관측성·평가 문서의 정의를 따른다.

6. 실패 신호 — 어떤 불변식이 깨졌는지 본다

섹션 제목: “6. 실패 신호 — 어떤 불변식이 깨졌는지 본다”
관측 증상먼저 의심할 설계 결함확인할 불변식
같은 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 effectguard가 순수하고 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 중복인지 소유 계층을 나눈다.

7. 선택 부록 — 제품별 확인 절차

섹션 제목: “7. 선택 부록 — 제품별 확인 절차”
XState로 작은 FSM을 확인할 때
  1. 첫 회독은 retry_wait를 제외한 안정 상태 6개 이하로 machine definition을 만든다.
  2. 두 번째 회독에서 retry_wait와 retry 소진 전이를 추가해 숨은 반복이 드러나는지 본다.
  3. state × event 표에서 허용 전이와 거부 전이를 각각 테스트한다.
  4. APPROVE를 두 번 보내 두 번째 이벤트가 발송 effect를 만들지 않는지 확인한다.
  5. 평면 상태 이름이 조합식으로 늘어날 때만 hierarchical·parallel state로 옮긴다.
  6. XState action이라는 API 이름과 별개로 guard·internal update·external effect의 설계 책임이 분리됐는지 검토한다.
LangGraph checkpoint를 확인할 때
  1. judge -> revise loop에 총 실행 상한 max_attempts=3과 fallback node를 두고, attempt=1부터 시작해 실행이 세 번을 넘지 않는지 확인한다.
  2. super-step별 StateSnapshot에서 next node/task, attempt, artifact reference, trace_id가 이어지는지 확인한다.
  3. 승인 직전 checkpoint에서 재개해 이미 끝난 draft/tool node가 불필요하게 effect를 반복하지 않는지 본다. Checkpoint 자체가 외부 effect 완료를 보장한다고 가정하지 않는다.
  4. 큰 검색 결과 원문 대신 versioned artifact reference를 state에 두고 bundle은 호출마다 다시 투영한다.
  5. 최신 graph를 배포한 뒤 기존 thread가 호환되는지 확인하고, 구버전 고정이 필요하면 애플리케이션 routing과 drain 정책을 별도로 검증한다.
  6. 이 실습의 replay를 Temporal deterministic history replay의 증거로 해석하지 않는다.

상태 머신과 워크플로 그래프 체크

  • FSM을 state·event·transition·invalid transition으로 설명할 수 있다
  • boolean 조합보다 tagged state가 불가능한 상태를 줄이는 이유를 설명할 수 있다
  • support-agent의 중복 APPROVE 이벤트가 발송 effect를 반복하지 않게 설계할 수 있다
  • transition guard·internal update·external effect와 XState action 용어를 구분할 수 있다
  • LLM structured output을 deterministic guard로 다시 검증할 수 있다
  • retry·timeout·cancel·compensation을 명시적 상태와 전이로 모델링할 수 있다
  • checkpoint에 resume-critical state·artifact reference·trace_id를 두고 전체 로그 정본과 분리할 수 있다
  • durable state에서 호출별 context bundle을 투영하는 계약을 설명할 수 있다
  • LangGraph checkpoint 기반 재개가 Temporal의 durable event-history replay를 대체하지 않음을 설명할 수 있다
  • checkpoint가 과거 graph code 고정과 기존 thread 호환을 자동 보장하지 않음을 설명할 수 있다
  1. FSM은 가능한 상태와 전이를 명시하고, 나머지 조합을 거부해 불가능한 상태를 줄인다.
  2. Statechart는 중첩·병렬 상태를, workflow graph는 step·분기·반복 경로를 더 잘 드러낸다.
  3. Transition guard·internal update·external effect는 설계 책임이고, XState action은 internal update와 external effect 양쪽 구현을 포함할 수 있는 프레임워크 용어다.
  4. LangGraph checkpoint는 super-step 경계의 StateSnapshot이며, 외부 effect의 완료나 원자성을 보장하지 않는다.
  5. LangGraph checkpoint는 Temporal durable replay와 과거 graph code 고정을 자동 제공하지 않으며, 상세 replay 의미론은 다음 Temporal 문서가 소유한다.

최종 수정: 2026-07-11