LangGraph · A2A로 멀티 에이전트 오케스트레이션 구현하기 — 공유 메모리 설계부터 실전 코드까지
Claude Code를 쓰다 보면 어느 순간 한계가 느껴집니다. 복잡한 리팩토링을 맡기면 컨텍스트 윈도우가 꽉 차고, 테스트 작성과 문서화를 동시에 진행하면 에이전트가 이것저것 뒤죽박죽 섞어버리죠. 저도 처음엔 "그냥 프롬프트를 더 잘 쓰면 되지 않을까?" 싶었는데, 실무에서 규모가 커질수록 단일 에이전트 하나에 모든 걸 밀어 넣는 방식은 분명히 한계가 있었습니다.
그래서 요즘 주목받는 게 멀티 에이전트 오케스트레이션입니다. 각 에이전트가 자신만의 메모리와 역할을 갖고, 필요한 정보만 공유하며 협업하는 구조예요. 이 글을 끝까지 읽고 나면 LangGraph로 3-에이전트 파이프라인을 직접 구성해보고, A2A 프로토콜로 에이전트 간 능력 탐색을 구현하는 방법까지 손에 익힐 수 있습니다.
솔직히 말씀드리면, 멀티 에이전트는 "왠지 멋있어 보여서" 도입했다가 토큰 비용 폭탄을 맞는 경우도 꽤 많습니다. 그래서 이 글은 기술 소개에만 그치지 않고, 실무에서 어떤 트레이드오프가 있는지도 같이 다룹니다. LangChain이나 LangGraph를 한 번이라도 써보신 백엔드 개발자라면 바로 따라오실 수 있고, 처음 접하신다면 LangGraph 공식 문서의 Quick Start를 먼저 훑어보시면 더 수월합니다.
핵심 개념
멀티 에이전트 오케스트레이션이란
멀티 에이전트 오케스트레이션은 여러 AI 에이전트가 각자의 메모리와 컨텍스트를 독립적으로 유지하면서 하나의 공동 목표를 향해 협력하는 아키텍처입니다. 비유하자면 팀 프로젝트와 비슷해요. 팀원마다 자기 노트북과 전문 지식이 따로 있고, Notion이나 Confluence 같은 공유 문서를 통해 핵심 정보를 교환하는 식이죠.
구조를 이루는 세 가지 핵심 요소는 다음과 같습니다.
| 개념 | 설명 | 비유 |
|---|---|---|
| Private Memory | 각 에이전트의 역할에 최적화된 독립 메모리 | 팀원의 개인 노트북 |
| Shared Context | 모든 에이전트가 읽고 쓰는 공유 저장소 | 팀 공유 문서 |
| 오케스트레이터 | 전체 흐름을 조율하고 에이전트를 스폰하는 상위 에이전트 | 팀 리드 |
Private Memory(독립 에이전트 메모리): 각 에이전트가 자신의 역할에 특화된 단기·장기 메모리를 별도로 유지합니다. 다른 에이전트의 내부 상태에 직접 접근할 수 없고, 반드시 정해진 채널을 통해서만 소통합니다.
⚠️ 이럴 때만 써보세요: 독립적으로 병렬 처리할 수 있는 서브태스크가 2개 이상 명확히 분리되고, 각 태스크가 서로 다른 도메인 지식을 필요로 할 때 멀티 에이전트가 빛을 발합니다. 반대로 순차적이고 단순한 태스크라면, 단일 에이전트에 좋은 프롬프트를 붙이는 쪽이 훨씬 경제적입니다.
MCP와 A2A — 헷갈리기 쉬운 두 프로토콜
저도 처음 접했을 때 "둘 다 에이전트 관련 프로토콜 아닌가?" 싶어서 헷갈렸는데, 역할이 명확히 다릅니다.
[에이전트 A] ──A2A──▶ [에이전트 B]
│ │
MCP MCP
│ │
▼ ▼
[도구/DB/API] [도구/DB/API]- MCP (Model Context Protocol, Anthropic): 에이전트 ↔ 도구·데이터소스 연결 레이어. 에이전트가 외부 세계와 소통하는 인터페이스입니다.
- A2A (Agent2Agent Protocol, Google): 에이전트 ↔ 에이전트 통신 레이어. 에이전트끼리 능력을 탐색하고, 작업을 협상하고, 상태를 동기화하는 표준화된 메시지 포맷을 제공합니다.
핵심 인사이트: MCP와 A2A는 경쟁 관계가 아니라 보완 관계입니다. MCP로 도구를 연결하고, A2A로 에이전트끼리 협상하는 식으로 함께 씁니다.
2025년 4월 Google이 A2A를 발표했을 때 Microsoft, Salesforce, SAP, Atlassian 등 50개 이상 파트너사가 합류하며 화제가 됐습니다. 이후 Linux Foundation 오픈소스 프로젝트로 편입되어 벤더 중립성도 확보했고요. 다만 2025년 하반기에 접어들며 에이전트 생태계 대부분이 MCP 중심으로 수렴하는 흐름이 관측됩니다. A2A는 장기 실행 워크플로나 인간 개입이 필요한 시나리오에서 여전히 강점을 보이고 있어요.
공유 메모리 아키텍처 — 두 가지 핵심 패턴
여러 에이전트가 메모리를 공유할 때 가장 신경 써야 하는 건 일관성(consistency) 문제입니다. 실무에서 자주 맞닥뜨리는 상황인데, 두 에이전트가 동시에 같은 공유 상태를 수정하면 충돌이 생깁니다. 실전에서 많이 쓰이는 두 가지 패턴을 먼저 짚고 가겠습니다.
| 패턴 | 방식 | 장점 | 단점 |
|---|---|---|---|
| 직렬화 턴 | 에이전트가 순서대로 공유 메모리에 접근 | 구현 단순, 충돌 없음 | 병렬성 포기 |
| 의미론적 잠금 | 벡터 DB에서 "이미 알려진 것"을 먼저 조회 후 중복 쓰기 방지 | 중복 분석 방지 | 쿼리 오버헤드 |
세 번째로 바이파타이트 접근 그래프(사용자-에이전트-리소스를 동적 그래프로 연결해 세밀한 읽기/쓰기 정책 적용)가 있는데, 솔직히 실무에서는 거의 안 씁니다. 오버엔지니어링이 대부분이에요. 심화 아키텍처가 필요하다면 Collaborative Memory 논문을 참고해보시면 좋겠습니다.
리듀서(Reducer) 패턴: 여러 에이전트가 동시에 상태를 업데이트할 때, 각 업데이트를 어떻게 병합할지 정의하는 함수입니다. LangGraph에서
Annotated[list, operator.add]처럼 선언하면 리스트에 항목이 자동으로 누적됩니다. 동시 쓰기 충돌을 방지하는 가장 단순하고 안전한 방법입니다.
블랙보드 아키텍처도 빠질 수 없습니다. 공유 칠판(블랙보드)에 에이전트들이 읽고 쓰며 문제를 함께 풀어가는 패턴인데, 오케스트레이터가 블랙보드 상태를 보고 다음에 어떤 에이전트를 활성화할지 동적으로 결정합니다. 실제 연구에서 9개 특화 에이전트가 블랙보드 기반으로 협업하는 소프트웨어 설계 시스템을 구현한 사례도 있습니다.
실전 적용
예시 1: Claude Code로 코드베이스 병렬 처리하기
가장 먼저 Python으로 멀티 에이전트의 핵심 구조, 즉 오케스트레이터가 팀원 에이전트를 스폰하고 블랙보드로 상태를 공유하는 패턴을 직접 구현해 봅니다. LangGraph 없이 바닐라 anthropic SDK로 작성한 예시라 구조 파악이 더 쉬울 거예요.
# Claude Code 멀티 에이전트 — 오케스트레이터 + 팀원 에이전트 구성 예시
import anthropic
import concurrent.futures
client = anthropic.Anthropic()
# 오케스트레이터: 전체 작업을 분석하고 서브태스크로 분해
orchestrator_prompt = """
당신은 소프트웨어 엔지니어링 팀의 오케스트레이터입니다.
주어진 코드베이스 변경 요청을 분석하고, 다음 세 에이전트에게 작업을 분배합니다:
- test_agent: 테스트 케이스 작성 담당
- refactor_agent: 코드 리팩토링 담당
- doc_agent: 문서화 담당
각 에이전트는 독립된 컨텍스트를 가지며, blockers 리스트를 통해 이슈를 공유합니다.
"""
def spawn_agent(role: str, task: str, shared_context: dict) -> str:
agent_prompts = {
"test_agent": "당신은 테스트 전문가입니다. 주어진 코드의 엣지 케이스를 중심으로 테스트를 작성해 주세요.",
"refactor_agent": "당신은 리팩토링 전문가입니다. 가독성과 성능을 함께 고려해 코드를 개선해 주세요.",
"doc_agent": "당신은 기술 문서 전문가입니다. 개발자가 바로 이해할 수 있는 문서를 작성해 주세요.",
}
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=4096,
system=agent_prompts[role],
messages=[
{
"role": "user",
"content": f"작업: {task}\n\n공유 컨텍스트:\n{shared_context}"
}
]
)
return response.content[0].text
# 공유 컨텍스트 (블랙보드 역할)
shared_context = {
"codebase_summary": "Python FastAPI 기반 REST API 서버",
"changed_files": ["src/user_service.py", "src/auth.py"],
"blockers": [] # 에이전트들이 공유하는 블로커 리스트
}
tasks = {
"test_agent": "user_service.py의 유닛 테스트 작성",
"refactor_agent": "auth.py의 토큰 검증 로직 리팩토링",
"doc_agent": "변경된 API 엔드포인트 문서화"
}
# 세 에이전트를 병렬로 실행
results = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
futures = {
executor.submit(spawn_agent, role, task, shared_context): role
for role, task in tasks.items()
}
for future in concurrent.futures.as_completed(futures):
role = futures[future]
results[role] = future.result()| 구성 요소 | 역할 | 메모리 범위 |
|---|---|---|
orchestrator_prompt |
전체 작업 분해 및 에이전트 배정 | 전체 코드베이스 맥락 |
spawn_agent() |
역할별 에이전트 실행 | 개별 독립 컨텍스트 |
shared_context |
블랙보드 역할, 공유 상태 | 모든 에이전트 읽기/쓰기 |
blockers |
에이전트 간 이슈 공유 채널 | 에이전트 간 mailbox 시뮬레이션 |
예시 2: LangGraph로 공유 벡터 메모리 기반 멀티 에이전트 구성하기
예시 1이 "에이전트 병렬 실행" 자체를 보여줬다면, 이번엔 에이전트들이 공유 상태를 어떻게 구조적으로 읽고 쓰는지를 LangGraph의 StateGraph로 표현합니다. 금융이나 법률처럼 전문 도메인에서는 각 에이전트가 역할별 임베딩 벡터 저장소를 보유하면서, 공유 벡터 DB(Qdrant, Weaviate 등)에 팩트를 기록·조회하는 패턴이 유효합니다. 중복 분석을 방지하고, 이전에 에이전트가 파악한 사실을 재활용할 수 있어서 실무에서 꽤 효과적이에요.
# LangGraph 기반 공유 벡터 메모리 멀티 에이전트 예시
# 필요 패키지: pip install langgraph qdrant-client sentence-transformers
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
from typing import TypedDict, Annotated
import operator
# 공유 상태 스키마 정의 — 모든 에이전트가 읽고 쓰는 블랙보드
class SharedState(TypedDict):
query: str
facts: Annotated[list, operator.add] # 리듀서: 여러 에이전트가 추가 가능
analysis_results: Annotated[list, operator.add]
final_answer: str
active_agent: str
# ── 아래 함수들은 실제 구현이 필요한 인터페이스입니다 ──
# search_shared_vector_db(query): Qdrant 컬렉션에서 유사 팩트 조회
# store_to_shared_vector_db(facts): 새 팩트를 Qdrant에 저장
# collect_facts(query): LLM 또는 외부 API로 신규 팩트 수집
# load_private_memory(agent_id): 에이전트별 전용 벡터 저장소에서 도메인 지식 로드
# run_analysis(facts, knowledge): 수집된 팩트 + 도메인 지식으로 분석 실행
# synthesize(results): 복수 분석 결과를 최종 답변으로 합성
# ─────────────────────────────────────────────────────────
# 리서치 에이전트 — 사실 수집 전담
def research_agent(state: SharedState) -> dict:
# 공유 벡터 DB에서 기존 팩트 조회 (의미론적 잠금 패턴)
existing_facts = search_shared_vector_db(state["query"])
if existing_facts:
# 이미 알려진 사실이 있으면 중복 분석 건너뜀
return {"facts": existing_facts, "active_agent": "analysis_agent"}
# 새 팩트 수집 후 공유 벡터 DB에 저장
new_facts = collect_facts(state["query"])
store_to_shared_vector_db(new_facts)
return {"facts": new_facts, "active_agent": "analysis_agent"}
# 분석 에이전트 — 사실 기반 분석 전담 (독립 장기 메모리 활용)
def analysis_agent(state: SharedState) -> dict:
# 에이전트 자신의 Private Memory에서 도메인 지식 로드
domain_knowledge = load_private_memory("analysis_agent")
analysis = run_analysis(state["facts"], domain_knowledge)
return {"analysis_results": [analysis], "active_agent": "synthesis_agent"}
# 종합 에이전트 — 최종 답변 합성
def synthesis_agent(state: SharedState) -> dict:
final = synthesize(state["analysis_results"])
return {"final_answer": final, "active_agent": "end"}
# 라우팅 로직 — 오케스트레이터가 블랙보드 상태를 보고 다음 에이전트 결정
def route_next(state: SharedState) -> str:
routing_map = {
"analysis_agent": "analysis_agent",
"synthesis_agent": "synthesis_agent",
"end": END
}
return routing_map.get(state["active_agent"], END)
# 그래프 구성
from langgraph.graph import END # END는 langgraph.graph에서 임포트
workflow = StateGraph(SharedState)
workflow.add_node("research_agent", research_agent)
workflow.add_node("analysis_agent", analysis_agent)
workflow.add_node("synthesis_agent", synthesis_agent)
workflow.set_entry_point("research_agent")
workflow.add_conditional_edges("research_agent", route_next)
workflow.add_conditional_edges("analysis_agent", route_next)
workflow.add_conditional_edges("synthesis_agent", route_next)
# 체크포인트 — 중단 후 재개 가능, 디버깅에도 활용
checkpointer = MemorySaver()
app = workflow.compile(checkpointer=checkpointer)위 코드에서 핵심은 Annotated[list, operator.add]로 선언된 공유 상태 필드입니다. 이 리듀서 패턴을 활용하면 여러 에이전트가 동시에 같은 리스트에 항목을 추가해도 병합이 자동으로 처리됩니다.
의미론적 잠금(Semantic Lock): 에이전트가 새 정보를 공유 메모리에 쓰기 전에 "이미 비슷한 내용이 있는지" 벡터 유사도 검색으로 먼저 확인하는 패턴입니다.
search_shared_vector_db가 바로 이 역할을 담당합니다. 실제 구현은docker run -p 6333:6333 qdrant/qdrant로 로컬 Qdrant를 띄운 뒤qdrant-client라이브러리로 연동할 수 있습니다.
예시 3: A2A 프로토콜로 에이전트 간 능력 탐색 구현하기
앞선 두 예시가 단일 시스템 내부의 에이전트 협업이었다면, A2A는 서비스 경계를 넘어 외부 에이전트와 협력할 때의 프로토콜입니다. 독립적인 두 시스템이 서로의 내부 구현 없이 표준 인터페이스만으로 협력해야 할 때 — 예를 들어 자사 주문 에이전트가 파트너사의 재고 에이전트에게 최적 발주량을 물어보는 상황 — 에 A2A가 빛을 발합니다. 아래 TypeScript 예시는 Node.js 18+ 환경을 기준으로 합니다(crypto.randomUUID와 fetch 모두 Node 18부터 내장).
// A2A 프로토콜 — Agent Card (능력 광고) 예시
// 실행 환경: Node.js 18+ (crypto.randomUUID, fetch 내장)
const inventoryAgentCard = {
name: "inventory-optimizer-agent",
version: "1.0.0",
description: "실시간 재고 최적화 및 발주 예측 전담 에이전트",
capabilities: {
streaming: true, // SSE 기반 스트리밍 지원
pushNotifications: true, // 비동기 알림 지원
stateTransitionHistory: true
},
skills: [
{
id: "optimize-stock-level",
name: "재고 수준 최적화",
description: "현재 재고 데이터를 바탕으로 최적 발주량 계산",
inputModes: ["application/json"],
outputModes: ["application/json", "text/plain"]
},
{
id: "predict-demand",
name: "수요 예측",
description: "과거 판매 데이터 기반 수요 예측",
inputModes: ["application/json"],
outputModes: ["application/json"]
}
]
}
interface ProductData {
productId: string
currentStock: number
salesHistory: number[]
}
interface OptimizationResult {
recommendedOrderQuantity: number
predictedDemand: number
}
// A2A 클라이언트 — 다른 에이전트에게 작업 위임
async function delegateToInventoryAgent(
productData: ProductData,
agentEndpoint: string
): Promise<OptimizationResult> {
const taskRequest = {
id: crypto.randomUUID(),
message: {
role: "user",
parts: [{
type: "data",
data: productData
}]
}
}
// A2A는 내부 메모리·도구를 노출하지 않고 표준 인터페이스로만 협력
const response = await fetch(`${agentEndpoint}/tasks/send`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(taskRequest)
})
return await response.json() as OptimizationResult
}보도에 따르면 Tyson Foods, Gordon Food Service 같은 식품 기업들이 공급망 최적화에 이와 유사한 패턴을 활용하고 있다고 합니다. 각 기업의 재고 에이전트가 내부 상태는 감추면서 표준화된 채널로만 제품 데이터와 최적화 정보를 교환하는 구조예요.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 병렬 처리 | 독립 에이전트가 동시 실행되어 전체 처리 시간이 크게 줄어듭니다 |
| 역할 특화 | 각 에이전트가 도메인 특화 메모리를 유지해 정확도가 올라갑니다 |
| 격리성 | 에이전트 간 메모리 오염 없이 내부 상태를 보호할 수 있습니다 |
| 확장성 | 새 에이전트를 추가하는 것만으로 기능 확장이 가능하고 기존 워크플로 영향이 최소화됩니다 |
| 표준화 | A2A/MCP로 벤더 중립적인 에이전트 조합이 가능합니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 토큰 비용 | 3-에이전트 팀은 단일 에이전트 대비 2.5~4배 토큰 소모 | 에이전트 수를 최소화하고, 병렬화가 실제로 필요한 태스크에만 적용 |
| 공유 메모리 병목 | 중앙화 공유 메모리는 처리량 병목 및 단일 장애점 위험 | 샤딩 또는 분산 벡터 DB 사용, 읽기 캐시 레이어 추가 |
| 일관성 문제 | 여러 에이전트가 동시에 공유 상태를 수정할 때 충돌 발생 가능 | 리듀서 패턴, 의미론적 잠금, 직렬화 턴 중 상황에 맞게 선택 |
| 오케스트레이션 복잡도 | 에이전트 수 증가에 따라 조율 로직이 기하급수적으로 복잡해짐 | 에이전트 3개 이하로 시작해 점진적으로 확장 |
| 디버깅 난이도 | 분산 에이전트 추적 및 재현이 단일 에이전트보다 어려움 | LangSmith, 체크포인트 기반 재현, 구조화 로깅 필수 |
| A2A 생태계 불확실성 | 2025년 하반기 기준 MCP로 생태계 수렴, A2A 채택 속도 둔화 | A2A는 장기 실행 워크플로에 한정 사용, 범용 연결은 MCP 우선 고려 |
실무에서 세 번째 항목(일관성 문제)이 가장 자주 발목을 잡더라고요. 처음엔 단순해 보이는데, 에이전트를 늘릴수록 누가 언제 공유 상태를 건드리는지 추적하기 어려워집니다. 초반에 리듀서 패턴을 제대로 잡아두는 게 나중에 큰 차이를 만듭니다.
실무에서 가장 흔한 실수
-
처음부터 에이전트를 너무 많이 만드는 것 — 3개 이하의 에이전트로 시작해 실제로 병렬화가 필요한 지점을 확인한 후 확장하는 방식이 훨씬 안전합니다. 에이전트가 많을수록 토큰 비용과 조율 복잡도가 동시에 증가합니다.
-
공유 컨텍스트에 너무 많은 정보를 담는 것 — 모든 에이전트가 모든 정보를 공유할 필요는 없습니다. "이 에이전트가 정말 이 정보가 필요한가?"를 매번 물어보시면 좋습니다. 공유 컨텍스트가 비대해지면 모든 에이전트의 토큰 소모가 동시에 늘어납니다.
-
관측성(observability)을 나중으로 미루는 것 — 에이전트 하나가 잘못된 결과를 냈을 때 어떤 에이전트가, 어떤 입력으로, 어떤 출력을 냈는지 추적할 수 없으면 디버깅이 거의 불가능합니다. LangSmith 같은 트레이싱 도구나 체크포인트 저장은 처음부터 붙여두는 편이 좋습니다.
마치며
멀티 에이전트 오케스트레이션은 "에이전트를 여럿 쓰는 것"이 아니라, 각자의 메모리를 지키면서 꼭 필요한 정보만 공유하는 설계 철학입니다.
지금 바로 시작해볼 수 있는 3단계:
-
LangGraph로 2-에이전트 파이프라인 구성해보기 —
pip install langgraph로 설치 후, 위 예시 코드의research_agent와analysis_agent두 개만으로 간단한 파이프라인을 만들어보시면 공유 상태와 라우팅 개념을 손으로 익힐 수 있습니다. -
Qdrant 로컬 인스턴스로 공유 벡터 메모리 실험해보기 —
docker run -p 6333:6333 qdrant/qdrant로 로컬에 띄운 후, 예시 2의search_shared_vector_db와store_to_shared_vector_db를 실제 Qdrant 호출로 구현해보시면 의미론적 잠금 패턴을 직접 체감할 수 있습니다. -
A2A Agent Card를 자신의 서비스에 붙여보기 — 예시 3의
inventoryAgentCard구조를 참고해 자신이 만드는 에이전트의 능력을 JSON으로 정의해보시면 좋습니다. 외부 에이전트와 협력할 일이 생길 때 바로 쓸 수 있는 기반이 됩니다.
공유 메모리 충돌에서 최후의 중재자는 결국 사람이어야 합니다 — 다음 글에서는 LangGraph의 Human-in-the-Loop 패턴, 즉 에이전트가 중요한 결정을 내리기 전에 사람에게 승인을 요청하는 인터럽트 설계 전략을 다뤄볼 예정입니다.
다음 글: LangGraph의 Human-in-the-Loop 패턴 — 에이전트가 중요한 결정을 내리기 전에 사람에게 승인을 요청하는 인터럽트 설계 전략
참고 자료
- Announcing the Agent2Agent Protocol (A2A) - Google Developers Blog
- What Is Agent2Agent (A2A) Protocol? | IBM
- A2A Protocol Explained: Secure Interoperability for Agentic AI 2026 - OneReach
- Empowering multi-agent apps with the open A2A protocol - Microsoft Cloud Blog
- MCP vs A2A: Protocols for Multi-Agent Collaboration 2026 - OneReach
- GitHub - a2aproject/A2A
- What happened to Google's A2A? - fka.dev
- Intrinsic Memory Agents: Heterogeneous Multi-Agent LLM Systems - arXiv
- Collaborative Memory: Multi-User Memory Sharing in LLM Agents - arXiv
- A-MEM: Agentic Memory for LLM Agents - arXiv
- AI Agent Memory: Comparative Analysis of LangGraph, CrewAI, AutoGen - DEV Community
- Claude Code Agent Teams: Multi-Agent Development Guide - Lushbinary
- Multi-Agent Coordination Patterns: Architectures Beyond the Hype - Medium
- Building Intelligent Multi-Agent Systems with MCPs and the Blackboard Pattern - Medium