
직렬 처리와 그래프 처리의 경계선
요약
AI 에이전트 설계 시 직렬 처리(Chain)와 그래프 처리(Graph)를 선택하는 기준을 다룹니다. 실행 순서가 곧 사양인 경우에는 직렬 처리를, 실행 경로 자체를 검증해야 하는 복잡한 워크플로에는 그래프 처리를 권장합니다.
핵심 포인트
- 직렬 처리는 고정된 순서와 입출력 안정성이 중요할 때 적합함
- 그래프 처리는 분기, 순환, 상태 합류 등 복잡한 경로 제어가 필요할 때 사용함
- 그래프 처리는 경로 제어를 명시적으로 만들지만 검증 비용이 증가함
- 에이전트는 절차와 도구 선택을 모델에 위임하는 별도의 개념임
AI 에이전트의 워크플로를 만들다 보면, 이른 단계에서 "이것은 그래프 처리 (Graph Processing)로 작성해야 하지 않을까?"라고 생각되는 순간이 있습니다.
분기가 있다. 되묻기가 있다. 인간의 승인이 있다. 도중에 다른 도구로 넘기고 싶다. 마지막에는 공통의 답변 생성으로 돌아가고 싶다. 그러한 요구사항을 보면, 일직선의 직렬 처리 (Serial Processing)보다 그래프 처리가 더 올바르게 보입니다.
다만, 처음부터 그래프 처리로 만들면, 설계가 영리해지기도 전에 검증 대상이 늘어납니다.
실행 순서를 읽으면 사양을 알 수 있다면 직렬 처리로 시작한다. 실행 경로 그 자체를 테스트 대상으로 삼지 않으면 유지보수할 수 없다면 그래프 처리를 검토한다.

이 기사에서는 LangChain / LangGraph적인 설계를 염두에 두고, 직렬 처리 (Chain)와 그래프 처리 (Graph)의 경계선만을 다룹니다.
직렬 처리와 그래프 처리를 「상하 관계」로 보지 말 것
먼저, 직렬 처리와 그래프 처리를 "그래프 처리가 더 고등하다"라고 보지 않는 것이 좋습니다.
역할을 대략 나누면 다음과 같습니다.
직렬 처리: 처리 순서가 거의 고정되어 있으며, 위에서 아래로 읽으면 사양을 알 수 있음 -
그래프 처리: 분기, 합류, 재시도, 상태 합류, 도달성을 명시적으로 다룸 -
에이전트 (Agent): 절차나 도구 선택 그 자체를 모델에 위임함

LangGraph의 공식 문서에서도, 그래프 처리는 상태 (State), 처리점 (Processing Point), 전이선 (Transition Line)을 중심으로, 경로 배분, 분기, 순환이 있는 상태를 가진 플로 (Flow)를 나타내는 것으로 설명되어 있습니다. 즉, 그래프 처리는 복잡한 경로를 표현하기 위한 도구입니다.
하지만, 표현할 수 있는 것과 채택해야 하는 것은 같지 않습니다.
그래프 처리로 만들면, 처리점과 전이선을 쓰는 것만으로 끝나지 않습니다. 배분기가 올바른 가지로 나아가는가. 도달 불가능한 처리점은 없는가. 순환은 멈추는가. 여러 경로에서 돌아왔을 때 상태가 망가지지 않는가. 실패 시 통과 경로를 복원할 수 있는가.
이것들도 설계 대상이 됩니다.
그러므로, 그래프 처리는 직렬 처리의 상위 호환이 아닙니다.
그래프 처리는, 경로 제어를 검증 가능하게 만드는 대신, 검증 비용도 떠안는 설계입니다.
리뷰에서 "이 절차의 입력과 출력은 무엇인가"를 보는 것만으로 충분하다면, 직렬 처리가 좋습니다. 반대로, "이 입력은 어떤 경로를 통하는가", "이 처리점은 정말로 도달하는가", "이 순환은 어디서 멈추는가"까지 확인하지 않으면 불안하다면, 그래프 처리를 검토해야 할 단계입니다.
직렬 처리로 충분한 상황
많은 업무 플로는 직렬 처리로 작성할 수 있습니다.
input
-> normalize
-> classify_intent
...

이 흐름에서 중요한 것은 "각 절차의 입출력이 안정적인가"입니다. 실행 경로 그 자체는 아직 복잡하지 않습니다.
분기가 조금 있더라도, 직렬 처리로 충분한 경우가 많습니다.
예를 들어 decide_response_policy 안에서 다음과 같이 판정하는 것뿐이라면, 그래프 처리로 만들 이유는 아직 약합니다.
- 정보가 부족하면 추가 질문으로 넘김
- 고위험 작업이라면 인간 확인으로 넘김
- 그 외에는 통상 답변으로 함
이 정도라면 함수 내의 조건 분기로 테스트할 수 있습니다. 처리 전체도 위에서 아래로 읽을 수 있습니다.
직렬 처리로 시작해도 좋은 신호는 다음과 같은 상태입니다.
- 처리 순서가 고정되어 있다
- 분기가 적다
- 분기 후의 합류가 거의 없다
- 각 절차의 입출력 스키마 (Schema)를 정의하면 리뷰할 수 있다
- 실패 시 "어느 절차에서 멈췄는지"를 알 수 있으면 충분하다
이 단계에서 그래프 처리화해도, 설계가 읽기 쉬워진다고는 할 수 없습니다. 오히려 처리점 이름, 전이선, 배분기, 상태 업데이트 규칙까지 읽어야 할 필요가 생깁니다.
직렬 처리의 장점은 책임의 위치가 명확하다는 것입니다. 분류가 이상하면 분류 절차를 본다. 검색 결과가 이상하면 검색 절차를 본다. 실패 위치가 처리 순서와 대응하므로, 초기 구현이나 작은 팀에서는 다루기 쉽습니다.
물론, 직렬 처리에도 한계는 있습니다. 조건 분기가 늘어나면, 하나의 절차가 "작은 그래프 처리"를 내부에 숨기기 시작합니다. 그 시점에서 직렬 처리의 읽기 쉬움은 떨어집니다. 그래프 처리로 옮겨가야 할 때는, 이 숨겨진 경로 제어를 외부로 꺼내고 싶어질 때입니다.
그래프 처리가 필요해지는 상황
그래프 처리가 필요해지는 것은, 분기, 합류, 순환, 상태 업데이트를 구현상의 검증 대상으로 분리하고 싶을 때입니다.
동일한 고객 지원 문의 플로를 그래프 처리로 작성하면, 예를 다음과 같습니다.
start
-> classify_intent
-> route_by_intent
...

중요한 것은, 그림으로서 보기 좋다는 것이 아닙니다. 배분기를 단독으로 확인할 수 있다는 것입니다.
- 「사용법을 알려줘」는
faq_lookup으로 진행하는가 - 「주문 12345를 취소하고 싶어」는
human_review로 진행하는가 - 「주문에 대해 상담하고 싶어」는
ask_clarification으로 진행하는가 - 주문 번호가 포함된 배송 확인은
order_lookup으로 진행하는가
이러한 기대 전이 (Expected Transition)를 테스트로 분리하고 싶다면, 그래프 처리 (Graph Processing)를 검토할 가치가 있습니다.
나아가, 그래프 처리가 진가를 발휘하는 지점은 합류 (Merge) 단계입니다.
FAQ 검색, 주문 검색, 사람의 확인, 추가 질문이 각각 별도의 경로로 나뉜 후, 공통의 답변 생성 처리점으로 돌아옵니다. 이때 각 경로가 어떤 상태 (State)를 가지고 돌아올지를 결정해 두어야 합니다.
예를 들어 retrievedContext라는 항목 이름에, 어떤 경로에서는 문자열을 넣고 다른 경로에서는 주문 데이터를 넣는다고 가정해 봅시다. 규모가 작을 때는 동작할지도 모릅니다. 하지만 나중에 답변 생성 처리점을 읽는 사람은 해당 항목 이름이 무엇을 의미하는지 혼란을 겪게 됩니다.
그래프 처리가 필요한 상황에서는 처리점을 늘리는 것뿐만 아니라, 상태의 의미를 고정할 필요가 있습니다.
여기서 그래프 처리를 사용하는 가치는 실행 경로에 이름이 붙는다는 점입니다. faq_lookup, order_lookup, human_review와 같이 경로가 처리점 (Node)으로서 나타나면, 테스트 명칭, 로그, 리뷰 코멘트, 장애 조사 시 사용하는 용어가 일치하게 됩니다.
"취소 문의가 왜 일반 답변으로 넘어갔는가"라는 질문을 받았을 때, 그래프 처리라면 경로 기록을 확인할 수 있습니다. 직렬 처리 (Sequential Processing) 내부의 거대한 조건 분기 속에 파묻혀 있는 것보다 원인을 추적하기 쉬운 경우가 있습니다.
단, 이는 실행 기록을 저장하고 있을 때의 이야기입니다. 그래프 처리를 작성했다고 해서 저절로 디버깅이 쉬워지는 것은 아닙니다. 그래프 처리를 선택한다면, 통과하는 처리점의 시퀀스와 상태 차분 (State Delta)을 남기는 것까지 세트로 고려해야 합니다.
그래프 처리로 인해 늘어나는 검증 대상
그래프 처리를 채택하면 최소한 이 정도는 확인하고 싶어집니다.

| 검증 대상 | 확인 사항 |
|---|---|
| 분류 테스트 | 입력이 기대한 분기로 진행되는가 |
| ... |
그래프 처리 채택의 판단 기준은 "복잡한 것을 하고 싶다"가 아닙니다. 이러한 검증 비용을 지불할 만큼 경로 제어 (Path Control)가 중요한가입니다.
TypeScript로 무엇을 검증했는가
TypeScript를 사용하여 동일한 고객 지원 문의 플로우를 두 가지 방식으로 구현했습니다.
검증 코드는 아래 접기 섹션에 포함되어 있습니다.
- 직렬 처리 버전:
@langchain/core/runnables1.2.3의RunnableSequence로 6단계 실행 - 그래프 처리 버전:
@langchain/langgraph1.4.8로 11개 처리점, 13개 전이선, 1개 분류기 구현 - 구조 검증: LangGraph로 실행하여 경로, 도달성, 순환, 합류를
results_ts.json에 기록 - 모델 검증: Pi를 경유하는
openai-codex/gpt-5.4-mini로 동일한 5건을 경로 분류하여results_model_router.json에 기록 - 실행:
npm test/npm run model-eval
검증 코드는 총 5개 파일입니다. 읽는 순서는 접기 제목의 번호 순서대로입니다.
1. 구조 검증의 입구: support_flow_experiment.ts
import { writeFile } from "node:fs/promises";
import { FinalKind, ORACLE_CASES, OracleCase, RouteNode } from "./domain.js";
import { CHAIN_LIBRARY, chainSteps, runChain } from "./chain_flow.js";
...
2. 공유 어휘: domain.ts
/** 두 가지 처리 형태 모두에서 사용하는 문의 의도 분류. */
export type Intent = "faq" | "order" | "high_risk_order" | "unknown";
/** 직렬 처리 버전과 그래프 처리 버전에서 비교할 최종 결과 분류. */
...
3. 직렬 처리 버전: chain_flow.ts
import { RunnableLambda, RunnableSequence } from "@langchain/core/runnables";
import {
FAQ,
...
4. LangGraph 버전: graph_flow.ts
import { Annotation, END, START, StateGraph } from "@langchain/langgraph";
import {
FAQ,
...
5. 모델 채점: model_router_eval.ts
import { readFile, writeFile } from "node:fs/promises";
/** `model_router_raw_output.txt`를 생성하는 데 사용된 Pi 구독을 통한 모델. */
const MODEL_ID = "openai-codex/gpt-5.4-mini";
...
```json
*/i, "").replace(/^```\s*"/i, "").replace(/```$/i, "").trim();
}
function asModelRoute(value: unknown): ModelRoute {
if (!isObjectRecord(value)) throw new Error(`invalid model route: ${JSON.stringify(value)}`);
const candidate = value as { id?: unknown; route?: unknown };
if (typeof candidate.id !== "string" || typeof candidate.route !== "string") {
throw new Error(`invalid model route: ${JSON.stringify(value)}`);
}
return { id: candidate.id, route: candidate.route };
}
function isObjectRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function evaluateCase(oracleCase: ModelOracleCase, modelRoutes: ModelRoute[]): ModelCaseResult {
const modelRoute = modelRoutes.find((row) => row.id === oracleCase.id)?.route ?? "missing_model_route";
return { ...oracleCase, modelRoute, pass: modelRoute === oracleCase.expectedRoute };
}
const result = await runModelRouterEval();
const json = JSON.stringify(result, null, 2);
console.log(json);
await writeFile("results_model_router.json", `${json}\n`, "utf8");
if (!result.allModelRoutesPassed) process.exitCode = 1;
검증하고 싶었던 것은 성능 차이가 아닙니다. 다음 세 가지입니다.
...
input -> step1 -> step2 -> step3 -> output
다음으로, 분기만 나열하겠습니다.
- 어떤 입력에서 분기할지
- 어떤 상태에서 분기할지
- 분기처는 고정인지, 모델 판단인지
- 분기 후에 합류하는지
여기서 분기가 적고 합류도 단순하면 직렬 처리로 시작합니다.
그래프 처리를 진행하는 것은, 직렬 처리로는 경로 제어를 안전하게 다룰 수 없게 된 후여야 합니다. 이상적인 상황은 '그래프 처리라면 할 수 있다'가 아니라, '직렬 처리에서는 여기가 검증하기 어려웠다'라는 증거를 남긴 후에 넘어가는 것입니다.
예를 들면, 다음과 같은 증거입니다.
- 직렬 처리 내의 조건 분기가 읽기 어려워진 경우
- 분기 추가로 기존 동작이 깨진 경우
- 합류 시 상태 항목의 의미가 충돌한 경우
- 실행 기록만으로는 원인 추적이 어려워진 경우
그래프 처리로 전환하는 설득력은, 그래프 처리의 표현력이 아니라, 직렬 처리에서 겪었던 구체적인 예시에서 나옵니다.
또 하나 중요한 것은 전환 후에 비교할 수 있도록 준비해 두는 것입니다. 직렬 처리 (Serial Processing) 버전에서 사용했던 대표 입력을 버리지 말고, 그래프 처리 (Graph Processing) 버전에서도 동일한 입력을 흘려보냅니다. 최종 출력이 같은지 확인합니다. 기대하는 경로가 있다면 그것도 확인합니다. 도달 불가능한 처리 지점(unreachable processing point)이나 최대 깊이(maximum depth)와 같이, 그래프 처리에서 비로소 보일 수 있는 검사 항목을 추가합니다.
이러한 순서를 따르면, 그래프 처리로의 전환은 '전면 재작성'이 아니라 '검증 대상의 추가'가 됩니다. 본문 내의 TypeScript 검증도 그 방식을 따르고 있습니다. 직렬 처리 버전과 그래프 처리 버전에 동일한 5개의 입력을 흘려보내 최종 출력의 일치 여부를 확인한 뒤, 그래프 처리 고유의 분기기 (router), 도달성 (reachability), 순환 (cycle), 합류 (merge)를 살펴보았습니다.
결론
망설여진다면, 우선 직렬 처리로 작성합니다.
그 후, 경로를 테스트 이름, 실행 기록 (execution log), 도달성 검사로 다루고 싶어질 때 그래프 처리로 넘어갑니다. 전환의 이유는 "그래프 처리가 더 고차원적이라서"가 아니라, "직렬 처리로는 검증하기 어려운 경로가 보였기 때문"이면 충분합니다.
이번 TypeScript 검증에서도 그 차이점이 나타났습니다. 직렬 처리는 6개의 단계를 순서대로 보기만 하면 됩니다. 그래프 처리는 분기, 도달성, 순환 상한 (cycle limit), 상태 합류 (state merge), 실행 기록을 각각 별도로 살펴봐야 합니다.
그래프 처리는 멋진 구조를 위한 것이 아닙니다. 경로 제어를 검사하기 위한 구조입니다.
출처
- LangGraph.js Graph API: https://docs.langchain.com/oss/javascript/langgraph/graph-api
- LangGraph.js quickstart: https://docs.langchain.com/oss/javascript/langgraph/quickstart
- LangGraph.js StateGraph API reference: https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph.StateGraph.html
- LangGraph.js Annotation API reference: https://langchain-ai.github.io/langgraphjs/reference/modules/langgraph.Annotation.html
- Pi GitHub repository: https://github.com/earendil-works/pi
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기