그래프 07. 병렬 실행 기초 — 첫 번째 Fan-out/Fan-in
학습 목표
이 튜토리얼을 마치면 다음을 할 수 있습니다.
- 2개 브랜치의 가장 단순한 팬아웃/팬인 패턴을 처음부터 작성할 수 있다.
parallel_groups의branches와join선언을 올바르게 작성할 수 있다.writes_state와reads_state를 올바른 위치에 선언할 수 있다.- 팬아웃과 팬인 엣지를 올바르게 연결할 수 있다.
graph validate로 모든 병렬 체크를 통과하는 그래프를 완성할 수 있다.- 병렬 관련 에러 코드 3가지(
PARALLEL_METADATA_DROPPED,PARALLEL_FINALIZE_BEFORE_JOIN,PARALLEL_STATE_SCHEMA_MISSING)를 직접 시연하고 수정할 수 있다.
사전 준비
mkdir -p examples/graphs/parallel
# 06_state_schema.md 완료 확인
euleragent graph validate examples/graphs/state/schema_demo.yaml
병렬 팬아웃/팬인 토폴로지
이 튜토리얼에서 구현할 기본 병렬 구조입니다.
source ──┬──→ branch_a ──┬──→ join_node → evaluate → finalize
└──→ branch_b ──┘
- 팬아웃(fan-out):
source에서branch_a와branch_b로 분기 - 병렬 실행:
branch_a와branch_b가 동시에 실행 - 팬인(fan-in):
branch_a와branch_b가 모두 완료된 후join_node로 합류 - 하류(downstream):
join_node이후는 순차 실행
단계별 실습
단계 1: 그래프 설계
구현할 그래프의 전체 구조를 먼저 설계합니다.
목적: 두 가지 소스에서 동시에 정보를 수집하고 합쳐서 평가합니다.
| 노드 | 역할 | 타입 |
|---|---|---|
source |
검색 방향 결정 | llm (execute) |
branch_a |
웹 소스 검색 | llm (execute) |
branch_b |
내부 DB 검색 | llm (execute) |
join_node |
결과 합산 및 초안 작성 | llm (execute) |
evaluate |
품질 평가 | judge |
revise |
수정 | llm (execute) |
state_schema 설계:
- results: list + append_list — 두 브랜치가 수집한 결과 목록
- summary: string + last_write — join_node가 작성하는 최종 요약
단계 2: state_schema 선언
state_schema:
results:
type: list
merge: append_list # 두 브랜치 결과를 하나의 리스트로 합산
summary:
type: string
merge: last_write # join_node만 씀 (단일 브랜치 → 안전)
단계 3: parallel_groups 선언
parallel_groups:
- id: search_group # 그룹 ID
branches: [branch_a, branch_b] # 병렬 실행할 브랜치 노드 ID 목록
join: join_node # 모든 브랜치 완료 후 실행할 조인 노드 ID
단계 4: 노드 선언 (writes_state, reads_state 포함)
병렬 브랜치 노드는 반드시 writes_state를 선언해야 합니다. 선언하지 않으면
PARALLEL_METADATA_DROPPED 에러가 발생합니다.
nodes:
- id: source
kind: llm
runner:
mode: execute
- id: branch_a
kind: llm
runner:
mode: execute
writes_state: [results] # ← 반드시 선언 (빈 리스트도 가능)
- id: branch_b
kind: llm
runner:
mode: execute
writes_state: [results] # ← branch_a와 같은 키 → append_list로 안전
- id: join_node
kind: llm
runner:
mode: execute
reads_state: [results] # ← 두 브랜치의 결과를 읽음
writes_state: [summary] # ← 최종 요약을 씀
artifacts:
primary: combined_report.md
- id: evaluate
kind: judge
judge:
schema: evaluator_v1
route_values: [finalize, revise]
- id: revise
kind: llm
runner:
mode: execute
artifacts:
primary: combined_report.md
단계 5: 엣지 연결 (팬아웃 + 팬인)
edges:
# 팬아웃: source → 두 브랜치
- from: source
to: branch_a
when: "true"
- from: source
to: branch_b
when: "true"
# 팬인: 두 브랜치 → join_node
- from: branch_a
to: join_node
when: "true"
- from: branch_b
to: join_node
when: "true"
# 하류: join_node 이후 순차 실행
- from: join_node
to: evaluate
when: "true"
- from: evaluate
to: finalize
when: "judge.route == finalize"
- from: evaluate
to: revise
when: "judge.route == revise"
- from: revise
to: evaluate
when: "true"
단계 6: 완전한 YAML 작성
이제 모든 조각을 하나로 조립합니다.
# examples/graphs/parallel/my_first_parallel.yaml
id: graph.my_first_parallel
version: 1
category: research
description: 2개 브랜치 병렬 검색 — source → [branch_a|branch_b] → join → evaluate
# 병렬 실행이 있으면 state_schema 필수
state_schema:
results:
type: list
merge: append_list # branch_a, branch_b 모두 이 키에 씀 → 합산
summary:
type: string
merge: last_write # join_node만 씀 → 안전
defaults:
max_iterations: 3
max_total_tool_calls: 30
max_web_search_calls: 5
# 병렬 그룹 선언
parallel_groups:
- id: search_group
branches: [branch_a, branch_b]
join: join_node
nodes:
- id: source
kind: llm
runner:
mode: execute
- id: branch_a
kind: llm
runner:
mode: execute
writes_state: [results] # ← 반드시 선언!
- id: branch_b
kind: llm
runner:
mode: execute
writes_state: [results] # ← branch_a와 같은 키
- id: join_node
kind: llm
runner:
mode: execute
reads_state: [results] # ← 두 브랜치의 수집 결과 읽기
writes_state: [summary]
artifacts:
primary: combined_report.md
- id: evaluate
kind: judge
judge:
schema: evaluator_v1
route_values: [finalize, revise]
- id: revise
kind: llm
runner:
mode: execute
artifacts:
primary: combined_report.md
edges:
# 팬아웃
- from: source
to: branch_a
when: "true"
- from: source
to: branch_b
when: "true"
# 팬인
- from: branch_a
to: join_node
when: "true"
- from: branch_b
to: join_node
when: "true"
# 하류 순차 실행
- from: join_node
to: evaluate
when: "true"
- from: evaluate
to: finalize
when: "judge.route == finalize"
- from: evaluate
to: revise
when: "judge.route == revise"
- from: revise
to: evaluate
when: "true"
finalize:
artifact: combined_report.md
단계 7: graph validate 실행
euleragent graph validate examples/graphs/parallel/my_first_parallel.yaml
예상 출력:
검증 중: examples/graphs/parallel/my_first_parallel.yaml
단계 1/3: YAML 파싱...
id: graph.my_first_parallel
노드 수: 6 (source, branch_a, branch_b, join_node, evaluate, revise)
엣지 수: 8
parallel_groups: 1개 (search_group)
완료
단계 2/3: Pattern 기본 검증...
[✓] 노드 ID 유일성
[✓] 엣지 소스/타겟 존재
[✓] finalize 도달 가능성
[✓] judge route_values 커버리지
[✓] 순환 감지: evaluate → revise → evaluate (max_iterations=3)
완료
단계 3/3: Graph 추가 검증...
[✓] state_schema 존재 (parallel_groups 사용 시 필수)
[✓] state_schema 타입+merge 호환성:
results: list + append_list ✓
summary: string + last_write ✓
[✓] parallel_groups 검증:
search_group:
branches: [branch_a, branch_b] (2개, 최대 8개)
join: join_node ✓ (노드 존재)
[✓] 각 브랜치 writes_state 선언:
branch_a: [results] ✓
branch_b: [results] ✓
[✓] 팬아웃 엣지:
source → branch_a ✓
source → branch_b ✓
[✓] 팬인 엣지 (브랜치 → 조인):
branch_a → join_node ✓
branch_b → join_node ✓
[✓] 브랜치가 finalize로 직접 라우팅 안 함 ✓
[✓] 브랜치 수 제한 (2 ≤ 8) ✓
[✓] 병렬 브랜치 부작용 도구 미사용 ✓
완료
결과: 유효 ✓ (오류 없음, 경고 없음)
모든 병렬 체크가 통과됩니다!
단계 8: graph compile로 IR 확인
euleragent graph compile examples/graphs/parallel/my_first_parallel.yaml \
--out /tmp/my_first_parallel_ir.json
# IR의 parallel_groups 섹션 확인
python -m json.tool /tmp/my_first_parallel_ir.json | \
python -c "
import sys, json
d = json.load(sys.stdin)
print('=== parallel_groups ===')
print(json.dumps(d['parallel_groups'], indent=2))
print()
print('=== langgraph_builder (병렬 엣지) ===')
lb = d['langgraph_builder']
print('add_edges:', json.dumps(lb['add_edges'], indent=2))
"
예상 출력:
=== parallel_groups ===
[
{
"id": "search_group",
"branches": ["branch_a", "branch_b"],
"join": "join_node",
"fanout_source": "source",
"fanin_target": "join_node"
}
]
=== langgraph_builder (병렬 엣지) ===
add_edges: [
["source", "branch_a"],
["source", "branch_b"],
["branch_a", "join_node"],
["branch_b", "join_node"],
["join_node", "evaluate"],
["revise", "evaluate"]
]
IR의 parallel_groups에 fanout_source와 fanin_target이 자동으로 추가됩니다. 이 정보는
LangGraph가 실제 병렬 실행 구조를 구축할 때 사용됩니다.
단계 9: 의도적 에러 시연
에러 시연 1: PARALLEL_METADATA_DROPPED
브랜치 노드에서 writes_state를 제거합니다.
# branch_a에서 writes_state 제거
- id: branch_a
kind: llm
runner:
mode: execute
# writes_state: [results] ← 제거!
# 임시 파일로 테스트 (원본 수정 대신)
cp examples/graphs/parallel/my_first_parallel.yaml /tmp/parallel_no_writes.yaml
# branch_a의 writes_state 제거 후 검증
euleragent graph validate /tmp/parallel_no_writes.yaml
예상 출력:
오류: PARALLEL_METADATA_DROPPED
parallel_groups 'search_group'의 브랜치 노드 'branch_a'에
writes_state가 선언되지 않았습니다.
writes_state는 병렬 브랜치에서 필수입니다. 상태를 쓰지 않는
브랜치라도 빈 리스트를 명시적으로 선언해야 합니다:
writes_state: []
이 요구사항은 브랜치의 상태 기여를 명시적으로 문서화하기
위한 것입니다.
수정 방법:
# 상태를 쓰지 않는 브랜치라도 명시적으로 선언
- id: branch_a
kind: llm
runner:
mode: execute
writes_state: [] # 빈 리스트로 명시적 선언
에러 시연 2: PARALLEL_FINALIZE_BEFORE_JOIN
브랜치에서 직접 finalize로 엣지를 추가합니다.
# 잘못된 엣지: branch_a → finalize (조인 노드를 건너뜀)
edges:
- from: branch_a
to: finalize # ← 직접 finalize? 금지!
when: "true"
예상 출력:
오류: PARALLEL_FINALIZE_BEFORE_JOIN
병렬 브랜치 'branch_a'가 finalize 노드로 직접 라우팅합니다.
이는 다른 브랜치(branch_b)가 완료되기 전에 그래프가 종료될 수 있어
데이터 손실이 발생합니다.
해결: 모든 브랜치는 반드시 조인 노드('join_node')를 거쳐야 합니다.
branch_a → join_node (올바름)
join_node → ... → finalize (올바름)
에러 시연 3: PARALLEL_STATE_SCHEMA_MISSING
최상위의 state_schema를 제거합니다.
# state_schema: (완전히 제거)
parallel_groups:
- id: search_group
branches: [branch_a, branch_b]
join: join_node
예상 출력:
오류: PARALLEL_STATE_SCHEMA_MISSING
parallel_groups가 선언되어 있지만 최상위에 state_schema가 없습니다.
병렬 실행에서 브랜치들이 공유 상태를 쓸 때 LangGraph는 리듀서가
필요합니다. state_schema 없이 실행하면 런타임에
INVALID_CONCURRENT_GRAPH_UPDATE 예외가 발생합니다.
해결:
state_schema:
results:
type: list
merge: append_list
# ... 각 브랜치의 writes_state 키 정의
예상 출력 요약
| 명령 | 예상 결과 |
|---|---|
graph validate my_first_parallel.yaml |
유효 ✓, 모든 병렬 체크 통과 |
graph compile my_first_parallel.yaml |
IR parallel_groups, fanout_source, fanin_target |
| writes_state 제거 후 validate | PARALLEL_METADATA_DROPPED |
| branch→finalize 직접 엣지 validate | PARALLEL_FINALIZE_BEFORE_JOIN |
| state_schema 제거 후 validate | PARALLEL_STATE_SCHEMA_MISSING |
병렬 체크리스트 (7개 필수 요건)
□ 1. state_schema 선언 (parallel_groups 사용 시 필수)
□ 2. 모든 브랜치 writes_state 선언 (빈 리스트도 가능)
□ 3. 조인 노드 존재 (parallel_groups.join에 지정)
□ 4. 팬아웃 엣지: source → 모든 브랜치 (하나라도 누락 시 에러)
□ 5. 팬인 엣지: 모든 브랜치 → 조인 노드 (하나라도 누락 시 에러)
□ 6. 브랜치가 finalize로 직접 연결 안 함
□ 7. 브랜치 수 2 이상, 8 이하
핵심 개념 요약
| 개념 | 설명 |
|---|---|
| 팬아웃(fan-out) | 소스 노드 → 다수 브랜치 노드로 분기 |
| 팬인(fan-in) | 다수 브랜치 노드 → 조인 노드로 합류 |
parallel_groups.branches |
동시 실행될 노드 ID 목록 (2~8개) |
parallel_groups.join |
모든 브랜치 완료 후 실행될 조인 노드 ID |
writes_state |
브랜치 노드가 쓰는 state_schema 키 목록 |
reads_state |
조인 노드가 읽는 state_schema 키 목록 |
| PARALLEL_METADATA_DROPPED | 브랜치 노드에 writes_state 없음 |
| PARALLEL_FINALIZE_BEFORE_JOIN | 브랜치가 조인 없이 직접 finalize |
| PARALLEL_STATE_SCHEMA_MISSING | parallel_groups 있지만 state_schema 없음 |
흔한 오류
오류 1: 팬아웃 엣지 누락 (PARALLEL_FANOUT_MISSING)
# 오류: source에서 branch_b로 가는 팬아웃 엣지 없음
edges:
- from: source
to: branch_a
when: "true"
# branch_b 팬아웃 엣지 없음!
오류: PARALLEL_FANOUT_MISSING
parallel_groups 'search_group'의 브랜치 'branch_b'로 향하는
팬아웃 엣지가 없습니다.
해결: source에서 branch_b로 향하는 엣지를 추가하세요.
오류 2: 팬인 엣지 누락 (PARALLEL_BRANCH_NOT_CONVERGING)
# 오류: branch_b에서 join_node로 가는 팬인 엣지 없음
edges:
- from: branch_a
to: join_node
when: "true"
# branch_b → join_node 엣지 없음!
오류: PARALLEL_BRANCH_NOT_CONVERGING
parallel_groups 'search_group'의 브랜치 'branch_b'가
조인 노드 'join_node'로 수렴하지 않습니다.
해결: branch_b에서 join_node로 향하는 엣지를 추가하세요.
오류 3: 조인 노드 중복 (PARALLEL_JOIN_MULTIPLE)
# 오류: parallel_groups에 join이 2개?
parallel_groups:
- id: grp
branches: [branch_a, branch_b]
join: join_node_1 # ← 하나만 가능!
# join: join_node_2 — YAML에서 중복 키는 마지막 값으로 덮어쓰임
경고: PARALLEL_JOIN_MULTIPLE
parallel_groups 'grp'에 join이 중복 선언되어 있습니다.
YAML에서 중복 키는 마지막 값(join_node_2)으로 덮어쓰입니다.
의도한 조인 노드를 하나만 명시하세요.
실습 과제
과제: 뉴스 분석 병렬 그래프
다음 구조로 병렬 뉴스 분석 그래프를 작성하세요.
plan → [domestic_news | international_news] → synthesize → finalize
요구사항:
- plan 노드: 분석 방향 결정 (execute mode)
- domestic_news 브랜치: 국내 뉴스 분석 (execute mode, writes_state: [articles])
- international_news 브랜치: 국제 뉴스 분석 (execute mode, writes_state: [articles])
- synthesize 조인 노드: 두 분석 종합 (execute mode, reads_state: [articles])
- state_schema: articles (list + append_list)
- 최종 결과물: daily_briefing.md
euleragent graph validate examples/graphs/parallel/news_analysis.yaml
euleragent graph compile examples/graphs/parallel/news_analysis.yaml
이전: 06_state_schema.md | 다음: 08_parallel_advanced.md