Flow Engineering: LLM 워크플로우부터 조직 아키텍처까지, 흐름을 설계하는 방법
코드베이스를 들여다보다 "이 로직은 왜 이렇게 꼬여 있지?"라는 생각이 드는 순간이 있습니다. 저도 몇 년 전에 새 기능 하나 배포하는 데 2주가 걸린 적이 있었는데, 처음엔 코드가 더러워서 그런 줄 알았습니다. 알고 보니 PR 하나가 세 팀의 리뷰를 거쳐야 했고, 공용 QA 환경이 하나뿐이라 배포 순서를 맞추는 데만 며칠이 날아갔더라고요. 그 근본 원인이 사실 '흐름이 막혀 있었다'는 걸 한참 뒤에야 깨달았습니다.
Flow Engineering은 코드 품질 문제가 아니라 "작업이 어디서 막히는가"를 설계 수준에서 다루는 접근 방식입니다. AI 에이전트 워크플로우에서도, 마이크로서비스 아키텍처에서도, 팀의 배포 파이프라인에서도 — 어디서나 흐름(Flow)을 가시화하고 병목을 제거하는 것이 핵심입니다. 실제로 Weights & Biases 사례를 보면 단순 프롬프트로 17% 정확도에 그치던 작업이 흐름을 설계한 후 91%까지 올라간 결과가 있을 만큼, 효과가 꽤 극적이에요.
이 글에서는 세 가지 맥락(AI/LLM 워크플로우, 소프트웨어 아키텍처, 조직 프로세스)에서 Flow Engineering이 어떻게 작동하는지, 그리고 LangGraph로 실제 코드를 짜면서 어떻게 적용하는지를 살펴봅니다.
핵심 개념
"흐름"이란 무엇인가
Flow Engineering을 처음 접하면 개념이 좀 추상적으로 느껴질 수 있습니다. 세 가지 맥락을 나란히 놓고 보면 공통 철학이 뚜렷해지거든요:
Flow Engineering의 핵심 철학: 작업(Work)은 병목 없이 가치를 향해 흘러야 한다. 설계의 출발점은 그 흐름이 어디서 막히는지를 가시화하는 것이다.
| 맥락 | 정의 | 핵심 대상 |
|---|---|---|
| AI/LLM 엔지니어링 | 복잡한 작업을 단계별로 분해하고 LLM이 자기 검증하도록 워크플로우 그래프를 설계 | 프롬프트 → 에이전트 노드/엣지 |
| 소프트웨어 아키텍처 | Wardley Mapping + DDD + Team Topologies를 결합하여 변화의 흐름에 최적화된 시스템 구축 | 조직 구조 ↔ 기술 구조 |
| 조직/프로세스 | Feature, Defect, Risk, Debt 네 가지 Flow Item이 배포 파이프라인을 얼마나 효율적으로 흐르는지 측정 | 사이클 타임(작업 시작~완료 시간), 처리량 |
세 맥락을 보면 결국 같은 질문을 하고 있다는 걸 알게 됩니다. "지금 작업이 어디서 막히는가?"
AI/LLM 맥락: 단일 프롬프트의 한계를 넘어서
"프롬프트만 잘 쓰면 되겠지"라고 생각하고 복잡한 작업을 하나의 LLM 호출로 처리하려다 불안정한 결과를 받아본 적, 아마 다들 한 번쯤 있을 겁니다. 비결은 단순합니다. 작업을 쪼개고, 각 단계에서 LLM이 자기 검증(Self-Refine)하도록 흐름을 설계하는 것입니다.
LangGraph는 이 흐름을 노드(Node)와 엣지(Edge)로 표현하는 그래프 기반 상태 머신입니다. v1.0이 출시된 이후 사이클(순환), 메모리, 도구 호출을 지원하는 사실상 표준 프레임워크로 자리잡았습니다.
from langgraph.graph import StateGraph, END
from typing import TypedDict
# 실행 전 초기화 필요:
# from langchain_openai import ChatOpenAI
# from langchain_community.tools import DuckDuckGoSearchRun
# llm = ChatOpenAI(model="gpt-4o")
# search_web = DuckDuckGoSearchRun().invoke
class ResearchState(TypedDict):
query: str
search_results: list[str]
draft: str
review_feedback: str
final_answer: str
def search_node(state: ResearchState) -> ResearchState:
results = search_web(state["query"])
return {**state, "search_results": results}
def draft_node(state: ResearchState) -> ResearchState:
draft = llm.invoke(f"다음 자료로 답변을 작성하세요: {state['search_results']}")
return {**state, "draft": draft.content}
def review_node(state: ResearchState) -> ResearchState:
# 자기 검증 — 초안을 비판적으로 검토
feedback = llm.invoke(f"이 답변의 문제점을 찾아주세요: {state['draft']}")
return {**state, "review_feedback": feedback.content}
def revise_node(state: ResearchState) -> ResearchState:
final = llm.invoke(
f"피드백을 반영해 개선하세요.\n초안: {state['draft']}\n피드백: {state['review_feedback']}"
)
return {**state, "final_answer": final.content}
graph = StateGraph(ResearchState)
graph.add_node("search", search_node)
graph.add_node("draft", draft_node)
graph.add_node("review", review_node)
graph.add_node("revise", revise_node)
graph.set_entry_point("search")
graph.add_edge("search", "draft")
graph.add_edge("draft", "review")
graph.add_edge("review", "revise")
graph.add_edge("revise", END)
app = graph.compile()Self-Refine: LLM이 자신의 출력을 스스로 비판하고 개선하는 패턴. 별도 검증 에이전트를 두거나 같은 모델을 다른 프롬프트로 재호출하는 방식으로 구현됩니다.
저는 처음에 review 노드를 빼고 바로 revise로 연결했다가, 검증 단계 없이 수정을 요청하니 LLM이 멀쩡한 초안을 오히려 망치는 상황을 겪었습니다. "뭘 개선해야 할지 모르는 상태에서 개선 요청"을 하는 격이었던 거죠. search → draft → review → revise 순서를 명시적으로 지키는 게 생각보다 훨씬 중요합니다.
소프트웨어 아키텍처 맥락: 변화의 흐름에 최적화된 시스템
LLM 워크플로우에서 "단계를 명시적으로 설계하라"는 원칙이 작동했다면, 소프트웨어 아키텍처에서도 같은 질문이 적용됩니다. "변경이 어디서 막히는가?" Susanne Kaiser의 Architecture for Flow는 이 질문에 세 가지 방법론을 결합해 답합니다:
| 방법론 | 역할 | 답하는 질문 |
|---|---|---|
| Wardley Mapping | 전략 지형 파악 | 어떤 컴포넌트를 직접 만들고, 어떤 걸 사야 하는가? |
| Domain-Driven Design | 문제 공간 분리 | 어떻게 경계를 나눌 것인가? |
| Team Topologies | 팀 인터랙션 설계 | 팀이 서로 어떻게 협력할 것인가? |
솔직히 처음엔 방법론 세 개를 왜 함께 써야 하는지 이해하기 어려웠는데, Wardley Mapping을 예시로 보니 바로 납득이 됐습니다. "인증(Auth) 모듈을 직접 만들어야 할까, Auth0 같은 SaaS를 사야 할까?"라는 질문을 Wardley Map 위에 놓으면, 인증은 이미 commodity(범용화된 컴포넌트) 영역입니다. 직접 만들어봤자 차별화도 안 되고 유지보수 부담만 생긴다는 뜻이에요. 반면 우리 서비스의 핵심 추천 알고리즘은 genesis(독자적 혁신 영역)에 가깝기 때문에 직접 만드는 게 맞습니다. 이 판단을 시스템 전체에 걸쳐 일관되게 할 수 있도록 도와주는 것이 Wardley Mapping의 역할입니다.
이 세 가지가 맞물릴 때, 코드 변경이 다른 팀의 승인 없이도 독립적으로 배포될 수 있는 구조가 만들어집니다.
조직/프로세스 맥락: Flow Framework와 네 가지 흐름 아이템
아키텍처를 잘 설계했더라도, 실제 작업이 파이프라인을 어떻게 흐르는지 측정하지 않으면 어디서 막히는지 알 수 없습니다. Mik Kersten의 Flow Framework는 네 가지 Flow Item이 배포 파이프라인을 얼마나 효율적으로 흐르는지 측정하는 방법론입니다:
Flow Items:
├── Feature → 새로운 비즈니스 가치 창출
├── Defect → 품질 문제 수정
├── Risk → 보안, 컴플라이언스, 아키텍처 부채
└── Debt → 기술 부채 해소Flow Metrics: Flow Velocity(단위 시간당 완료 아이템 수), Flow Time(아이템이 요청부터 배포까지 걸리는 시간), Flow Efficiency(전체 시간 중 실제 작업 비율), Flow Distribution(네 가지 아이템의 비율)으로 구성됩니다.
실전 적용
예시 1: 멀티에이전트 리서치 워크플로우
IBM의 LangGraph + Granite 사례에서 차용한 오케스트레이터-서브에이전트 구조입니다. 오케스트레이터가 작업을 분석하고, 전문화된 서브에이전트(검색, 코드 생성)가 병렬로 실행되며, 검증과 편집 단계에서 결과를 통합하는 흐름입니다.
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator
# 초기화 예시:
# from langchain_openai import ChatOpenAI
# from langchain_community.tools import DuckDuckGoSearchRun
# llm = ChatOpenAI(model="gpt-4o")
# search_tool = DuckDuckGoSearchRun()
class MultiAgentState(TypedDict):
task: str
task_analysis: str # 오케스트레이터가 생성한 작업 분석
search_result: str
code_result: str
validation_result: str
final_output: str
errors: Annotated[list[str], operator.add] # 병렬 노드에서 안전하게 누적
def orchestrator(state: MultiAgentState) -> dict:
"""작업을 분석하고 서브에이전트가 공유할 컨텍스트를 생성"""
analysis = llm.invoke(
f"다음 작업을 분석하고 핵심 요구사항을 요약하세요: {state['task']}"
)
return {"task_analysis": analysis.content}
def search_agent(state: MultiAgentState) -> dict:
"""검색 전문 에이전트 — 오케스트레이터 분석을 컨텍스트로 활용"""
result = search_tool.invoke(
f"{state['task_analysis']}\n\n검색 쿼리: {state['task']}"
)
return {"search_result": result}
def code_agent(state: MultiAgentState) -> dict:
"""코드 생성 전문 에이전트"""
result = llm.invoke(
f"분석: {state['task_analysis']}\n\n요구사항에 맞는 코드를 작성하세요: {state['task']}"
)
return {"code_result": result.content}
def validation_agent(state: MultiAgentState) -> dict:
"""검색과 코드 결과를 모두 받은 뒤 실행 — 두 브랜치가 완료되어야 진입"""
combined = f"검색: {state['search_result']}\n코드: {state['code_result']}"
validation = llm.invoke(f"다음 결과를 검증하세요: {combined}")
return {"validation_result": validation.content}
def editor_agent(state: MultiAgentState) -> dict:
"""최종 통합"""
final = llm.invoke(
f"""다음 결과를 통합하여 최종 답변을 작성하세요:
- 작업 분석: {state['task_analysis']}
- 검색 결과: {state['search_result']}
- 코드 결과: {state['code_result']}
- 검증 결과: {state['validation_result']}"""
)
return {"final_output": final.content}
builder = StateGraph(MultiAgentState)
builder.add_node("orchestrator", orchestrator)
builder.add_node("search", search_agent)
builder.add_node("code", code_agent)
builder.add_node("validation", validation_agent)
builder.add_node("editor", editor_agent)
builder.set_entry_point("orchestrator")
# 팬아웃: orchestrator → search, code 두 노드가 같은 슈퍼스텝에서 실행
builder.add_edge("orchestrator", "search")
builder.add_edge("orchestrator", "code")
# 팬인: search, code 모두 완료된 슈퍼스텝 이후에 validation 진입
builder.add_edge("search", "validation")
builder.add_edge("code", "validation")
builder.add_edge("validation", "editor")
builder.add_edge("editor", END)
multi_agent_app = builder.compile()LangGraph는 내부적으로 슈퍼스텝(superstep) 단위로 실행합니다. orchestrator 완료 후 search와 code가 같은 슈퍼스텝에서 동시에 실행되고, 두 노드가 모두 완료된 뒤에야 validation 슈퍼스텝이 시작되는 구조입니다. 덕분에 Annotated[list, operator.add] 같은 상태 병합 어노테이션만 챙겨두면 동기화 처리를 별도로 구현하지 않아도 됩니다.
한 가지 주의할 점은 task_analysis를 서브에이전트에서 실제로 활용해야 오케스트레이터를 둔 의미가 있다는 것입니다. 처음 이 패턴을 쓸 때 분석 결과를 상태에 넣어만 놓고 아무도 읽지 않아서, 오케스트레이터가 있으나 마나인 코드가 된 적이 있었거든요.
예시 2: Flow Metrics로 개발 병목 찾기
BDC(캐나다 개발은행) 팀이 Axify로 흐름을 측정했을 때 발견한 것처럼, 대부분의 지연은 개발 중이 아니라 개발 전후(기획 대기, QA 대기)에서 발생합니다. 간단한 Flow Metrics 측정 코드를 만들어보면 이런 모양입니다:
interface FlowItem {
id: string;
type: "feature" | "defect" | "risk" | "debt";
requestedAt: Date;
startedAt: Date | null;
completedAt: Date | null;
}
interface FlowMetrics {
flowTime: number; // 요청부터 완료까지 (일)
waitTime: number; // 요청부터 시작까지 (일)
activeTime: number; // 실제 작업 시간 (일)
flowEfficiency: number; // activeTime / flowTime × 100 (%)
}
function calculateFlowMetrics(item: FlowItem): FlowMetrics | null {
if (!item.startedAt || !item.completedAt) return null;
const msPerDay = 1000 * 60 * 60 * 24;
const flowTime = (item.completedAt.getTime() - item.requestedAt.getTime()) / msPerDay;
const waitTime = (item.startedAt.getTime() - item.requestedAt.getTime()) / msPerDay;
const activeTime = (item.completedAt.getTime() - item.startedAt.getTime()) / msPerDay;
const flowEfficiency = (activeTime / flowTime) * 100;
return { flowTime, waitTime, activeTime, flowEfficiency };
}
function analyzeBottleneck(items: FlowItem[]): void {
const metrics = items
.map((item) => ({ item, metrics: calculateFlowMetrics(item) }))
.filter((r) => r.metrics !== null);
const avgWaitTime =
metrics.reduce((sum, r) => sum + r.metrics!.waitTime, 0) / metrics.length;
const avgEfficiency =
metrics.reduce((sum, r) => sum + r.metrics!.flowEfficiency, 0) / metrics.length;
console.log(`평균 대기 시간: ${avgWaitTime.toFixed(1)}일`);
console.log(`평균 흐름 효율성: ${avgEfficiency.toFixed(1)}%`);
// Mik Kersten의 연구에서 지식 노동(knowledge work)의 Flow Efficiency는
// 통상 15~40% 범위이며, 15% 미만은 대기 시간이 압도적으로 많다는 신호다.
if (avgEfficiency < 15) {
console.log("⚠️ 병목 감지: 실제 작업보다 대기 시간이 압도적으로 많습니다.");
console.log("→ 기획·QA·리뷰 프로세스를 점검해보시면 좋습니다.");
}
}이 숫자를 보기 전까진 "개발자가 느린 것"처럼 보였던 문제가 실제로는 프로세스 대기였다는 걸 알게 되는 경험이 꽤 충격적이더라고요. Flow Efficiency가 10%대라는 건, 하루 8시간 중 48분만 실제로 코드를 작성하고 나머지는 누군가의 승인을 기다리거나 다음 스프린트를 기다리는 시간이라는 뜻이니까요.
장단점 분석
장점
| 항목 | 내용 | 주의점 |
|---|---|---|
| 병목 가시화 | 작업 흐름을 측정 가능한 단위로 분해하여 숨겨진 지연 구간을 발견할 수 있습니다 | 측정 지점을 잘못 설정하면 잘못된 병목을 가리킬 수 있습니다 |
| LLM 품질 향상 | 단계 분해 + 자기 검증으로 단일 프롬프트 대비 정확도를 크게 높일 수 있습니다 (17% → 91% 사례) | 노드가 많아질수록 디버깅과 비용이 함께 늘어납니다 |
| 팀 자율성 | 흐름 중심 설계는 팀 간 의존도를 줄여 독립 배포 가능성을 높입니다 | Team Topologies 도입은 조직 변화를 수반하므로 경영진 지원이 필요합니다 |
| 비즈니스 연계 | Flow Metrics로 엔지니어링 성과를 사업 임팩트와 직접 연결할 수 있습니다 | 지표 설계 시 비즈니스 연결성을 먼저 검증해야 왜곡을 막을 수 있습니다 |
| 확장성 | 병렬 에이전트 실행, 이벤트 드리븐 패턴으로 수평 확장이 용이합니다 | Observability 인프라 없이 수평 확장하면 장애 원인 추적이 어려워집니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 측정의 역설 | Flow Metrics를 잘못 설계하면 커밋 수처럼 의미 없는 지표를 최적화하는 왜곡이 생깁니다 | 지표 설계 시 비즈니스 임팩트와의 연결성을 먼저 검증하는 것을 권장합니다 |
| 복잡성 증가 | 멀티에이전트 워크플로우는 단일 LLM 호출보다 디버깅이 훨씬 어렵습니다 | Observability 인프라를 먼저 구축하고 에이전트를 추가하는 순서가 좋습니다 |
| 비용 증가 | LLM 호출을 체이닝할 때 토큰 비용이 선형 이상으로 증가할 수 있습니다 | 단계별 캐싱, 저렴한 모델 혼합 사용을 고려해볼 수 있습니다 |
| 조직 변화 | Team Topologies, Architecture for Flow 도입은 기술 문제가 아닌 조직 문화 변화를 동반합니다 | 경영진 지원 없이 기술팀 단독 추진은 실패 확률이 높습니다 |
| 요구사항 변동성 | 흐름이 안정화돼도 요구사항이 자주 바뀌면 워크플로우 재설계 비용이 발생합니다 | 변경 빈도가 높은 노드는 유연하게 교체 가능하도록 인터페이스를 분리해두면 좋습니다 |
Observability (관찰가능성): 시스템 내부 상태를 외부 출력(로그, 트레이스, 메트릭)으로 추론할 수 있는 정도. 에이전트 워크플로우에서는 어떤 노드에서 어떤 입력으로 어떤 결정이 났는지 추적하는 것이 핵심입니다. LangChain 생태계에서는 LangSmith, 범용으로는 OpenTelemetry를 많이 사용합니다.
실무에서 가장 흔한 실수
-
측정 없이 최적화부터 시작하는 것: "우리 팀 흐름이 느리다"는 느낌만으로 프로세스를 뜯어고쳤다가 실제 병목이 아닌 곳을 개선하는 경우가 많습니다. Flow Metrics를 먼저 수집하고, 데이터를 보고 나서 개선 지점을 결정하는 것을 권장합니다.
-
단일 LLM 호출에 너무 많은 걸 기대하는 것: 복잡한 작업을 하나의 호출로 처리하려다 불안정한 결과를 얻는 경우입니다. 작업 단계를 3~5개로 쪼개고 각 단계에서 검증하는 구조가 훨씬 안정적입니다. 위에서 본 것처럼, review 노드 하나를 추가하는 것만으로도 결과 품질이 확 달라질 수 있거든요.
-
Observability 없이 프로덕션에 에이전트를 배포하는 것: 로컬에서는 잘 돌아가던 워크플로우가 프로덕션에서 이상하게 동작할 때, 어느 노드에서 무슨 입력이 들어왔는지 볼 수 없으면 디버깅이 사실상 불가능합니다. LangChain 보고서에서 89%의 기업이 이미 Observability를 구현했다는 수치가 괜히 나온 게 아닙니다.
마치며
흐름을 설계한다는 건 결국 "지금 우리가 무엇을 기다리고 있는가"를 솔직하게 들여다보는 작업입니다. 그 대기 시간을 코드로, 프로세스로, 조직 구조로 제거해나가는 것이 Flow Engineering의 본질입니다. AI 에이전트 워크플로우든, 마이크로서비스 아키텍처든, 팀의 개발 프로세스든 — 어디서든 흐름이 막히는 지점을 먼저 가시화하고 거기서부터 설계를 시작해보시면 좋습니다.
자신의 상황에 맞게 지금 바로 시작해볼 수 있는 3단계입니다:
-
팀의 Flow Time부터 측정해볼 수 있습니다. GitHub/Jira/Linear에서 지난 3개월 이슈를 추출해 "요청 날짜 → 배포 날짜" 간격을 계산해보시면 좋습니다. Feature와 Defect 각각의 평균 Flow Time이 얼마인지, 어느 단계(기획 대기, 리뷰, QA)에서 시간이 가장 많이 걸리는지 바로 보입니다. 팀 프로세스가 느리다고 느낀다면 여기서 시작하는 것을 권장합니다.
-
LLM을 활용하고 있다면 하나의 복잡한 프롬프트를 3단계로 분해해볼 수 있습니다. "검색 → 초안 작성 → 검토" 혹은 "분석 → 생성 → 검증"처럼 나누고, LangGraph로 각 단계를 노드로 정의해 연결해보시면 정확도 변화를 바로 확인하실 수 있습니다. LLM 결과 품질이 불안정하다면 여기서 시작하는 것을 권장합니다.
-
아키텍처 수준에서 접근하고 싶다면 Susanne Kaiser의 Architecture for Flow 또는 Mik Kersten의 Project to Product를 읽어보시는 것을 권장합니다. 두 책 모두 개념이 구체적인 사례와 함께 설명되어 있어, 팀 내 논의를 시작하는 데 좋은 출발점이 됩니다. 팀 간 배포 의존성이 복잡하다면 여기서 시작하는 것을 권장합니다.
참고 자료
- Flow Engineering is All You Need | Medium
- What is Architecture for Flow? | Susanne Kaiser
- Architecture for Flow 공식 사이트
- Architecture for Flow | O'Reilly
- AFLOW: ICLR 2025 논문 | arXiv
- FLOW: Modularized Agentic Workflow Automation | OpenReview ICLR 2025
- LLM Workflows: Patterns, Tools & Production Architecture | Morph
- State of Agent Engineering | LangChain
- LangGraph 공식 문서
- Flow Framework 공식 사이트
- What are Flow Metrics? | DX
- Flow Architectures | O'Reilly
- Understanding flow in software development | Swarmia
- Dissecting 'architecting for fast, sustainable flow' | microservices.io