A2A 프로토콜: AI 에이전트끼리 대화하는 표준
AI 에이전트 하나만으로 모든 일을 처리할 수 있다면 좋겠지만, 현실은 그리 단순하지 않습니다. 기업 환경에서는 채용 담당 에이전트, 일정 관리 에이전트, 데이터 분석 에이전트처럼 각자 전문 분야가 다른 에이전트들이 따로 돌아가고 있는 경우가 많거든요. 문제는 이 에이전트들이 서로 다른 프레임워크로 만들어져서 직접 대화할 방법이 없다는 겁니다.
바로 이 문제를 해결하기 위해 Google이 내놓은 것이 A2A(Agent-to-Agent) 프로토콜입니다. 이름 그대로 AI 에이전트 간의 통신을 표준화하는 오픈 프로토콜인데요. 이 글에서는 A2A가 왜 필요한지, 어떻게 작동하는지, 그리고 기존의 MCP와는 어떤 관계인지 살펴보겠습니다.
A2A가 필요한 이유
지금까지 AI 에이전트를 연동하려면 대부분 커스텀 API를 만들거나 특정 프레임워크에 종속된 방식을 써야 했습니다. LangChain으로 만든 에이전트와 Semantic Kernel로 만든 에이전트가 협업해야 한다면, 둘 사이를 이어주는 접착 코드를 직접 짜야 했죠.
에이전트가 2~3개일 때는 어떻게든 되지만, 조직 전체에 수십 개의 에이전트가 흩어져 있으면 상황이 복잡해집니다. 각 에이전트 쌍마다 연동 코드를 따로 만들어야 하니 조합 폭발이 일어나고, 한쪽을 업데이트하면 다른 쪽도 깨지기 쉽습니다.
A2A는 이 문제를 HTTP, JSON-RPC, SSE(Server-Sent Events)라는 이미 검증된 웹 표준 위에 공통 프로토콜을 얹는 방식으로 해결합니다. 에이전트가 어떤 프레임워크로 만들어졌든, 어떤 언어로 작성되었든 A2A만 구현하면 서로 대화할 수 있게 됩니다.
MCP와 A2A는 어떻게 다른가
Model Context Protocol(MCP)을 아시는 분이라면 “이미 MCP가 있는데 A2A는 왜 또 필요하지?”라고 생각하실 수 있습니다.
둘은 해결하는 문제가 다릅니다. MCP가 에이전트에게 도구와 데이터를 연결해주는 프로토콜이라면, A2A는 에이전트끼리 서로 대화하기 위한 프로토콜입니다. 비유하자면 MCP는 작업자에게 공구를 건네주는 것이고, A2A는 작업자들이 서로 업무를 분담하고 결과를 주고받는 것입니다.
사용자 ↔ 클라이언트 에이전트 ─── A2A ──→ 원격 에이전트 A
─── A2A ──→ 원격 에이전트 B
│
├── MCP ──→ DB 도구
└── MCP ──→ API 도구
실제 시나리오를 생각해보면 이해가 쉽습니다. 채용 담당 에이전트가 후보자를 찾아야 할 때, 이력서 분석 에이전트에게 A2A로 작업을 요청하고, 그 이력서 분석 에이전트는 내부적으로 MCP를 통해 데이터베이스나 외부 API에 접근하는 식이죠. 둘은 경쟁 관계가 아니라 서로 다른 계층에서 협력하는 관계입니다.
Agent Card로 자기소개하기
A2A에서 에이전트가 가장 먼저 하는 일은 자기소개입니다.
Agent Card라는 JSON 문서를 통해 “나는 이런 일을 할 수 있어”라고 자신의 능력을 알려주는 건데요.
/.well-known/agent-card.json 경로에 호스팅하면 다른 에이전트가 이를 발견하고 읽을 수 있습니다.
{
"name": "이력서 분석 에이전트",
"description": "이력서를 분석하여 직무 적합도를 평가합니다",
"supportedInterfaces": [
{
"url": "https://resume-agent.example.com/a2a",
"protocolBinding": "jsonrpc+http",
"protocolVersion": "1.0"
}
],
"capabilities": {
"streaming": true,
"pushNotifications": true
},
"skills": [
{
"id": "resume-analysis",
"name": "이력서 분석",
"description": "PDF 또는 텍스트 이력서를 파싱하여 기술 스택, 경력, 학력 정보를 추출합니다",
"inputModes": ["application/pdf", "text/plain"],
"outputModes": ["application/json"]
}
]
}
핵심은 skills 배열입니다.
각 스킬에는 고유 ID, 이름, 설명뿐만 아니라 어떤 형식의 입력을 받고 어떤 형식으로 출력하는지도 명시되어 있어서, 클라이언트 에이전트가 이 정보를 보고 적합한 에이전트를 골라 작업을 맡길 수 있습니다.
Agent Card를 발견하는 방법은 크게 세 가지입니다.
가장 기본적인 방식은 /.well-known/agent-card.json URL로 직접 접근하는 것이고, 여러 에이전트를 모아놓은 레지스트리 서비스를 통해 검색할 수도 있습니다.
개발 환경에서는 설정 파일에 에이전트 주소를 직접 적어놓기도 합니다.
작업 주고받기
Agent Card로 적합한 에이전트를 찾았으면 이제 작업을 요청할 차례입니다.
A2A에서 작업 요청은 message/send 메서드로 이루어집니다.
{
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [
{
"kind": "text",
"text": "이 이력서를 분석해서 Python 개발 경력을 요약해줘"
},
{
"kind": "file",
"file": {
"name": "resume.pdf",
"mimeType": "application/pdf",
"bytes": "JVBERi0xLjQK..."
}
}
],
"messageId": "msg-001"
}
}
}
메시지 안의 parts 배열이 눈에 띄는데요.
텍스트, 파일, 구조화된 데이터 등 여러 종류의 콘텐츠를 한 메시지에 담을 수 있습니다.
텍스트 지시사항과 함께 PDF 파일을 보내거나, 이미지와 JSON 데이터를 한꺼번에 전달할 수도 있습니다.
응답은 Task 객체로 돌아옵니다.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"id": "task-abc-123",
"contextId": "ctx-xyz-789",
"status": {
"state": "completed",
"timestamp": "2025-04-09T10:30:00Z"
},
"artifacts": [
{
"artifactId": "artifact-001",
"name": "분석 결과",
"parts": [
{
"kind": "data",
"data": {
"pythonExperience": "7년",
"frameworks": ["Django", "FastAPI", "Flask"],
"summary": "시니어 Python 백엔드 개발자..."
}
}
]
}
],
"kind": "task"
}
}
Task에는 고유 id와 contextId가 있어서 같은 맥락에서 여러 번의 대화를 이어갈 수 있습니다.
결과물은 artifacts 배열에 담기는데, 각 아티팩트도 parts를 가지고 있어서 텍스트, 파일, JSON 등 다양한 형식으로 결과를 돌려줄 수 있습니다.
Task 라이프사이클
모든 작업이 한 번에 끝나는 건 아닙니다. 간단한 질문은 바로 답이 오지만, 복잡한 분석은 몇 분이 걸리기도 하고 중간에 사용자 입력이 필요한 경우도 있습니다. A2A는 이런 다양한 상황을 Task의 상태로 관리합니다.
submitted → working → completed
→ failed
→ canceled
→ input-required → (재개) → working → ...
submitted로 시작해서 working 상태를 거쳐 completed나 failed 같은 최종 상태에 도달하는 흐름입니다.
여기서 주목할 만한 상태가 input-required인데요.
에이전트가 작업을 진행하다가 추가 정보가 필요하면 이 상태로 전환하고, 클라이언트가 같은 taskId로 후속 메시지를 보내면 작업이 다시 재개됩니다.
항공편 예약 시나리오를 예로 들어보면 이렇습니다.
{
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [
{ "kind": "text", "text": "서울에서 도쿄로 가는 항공편을 찾아줘" }
],
"messageId": "msg-001"
}
}
}
{
"result": {
"id": "task-flight-001",
"status": {
"state": "input-required",
"message": {
"role": "agent",
"parts": [{ "kind": "text", "text": "출발 날짜를 알려주시겠어요?" }]
}
},
"kind": "task"
}
}
{
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{ "kind": "text", "text": "5월 15일이요" }],
"contextId": "ctx-flight",
"taskId": "task-flight-001",
"messageId": "msg-002"
}
}
}
contextId와 taskId를 이용해서 이전 대화의 맥락을 유지하면서 자연스럽게 멀티턴 대화를 이어가는 모습입니다.
스트리밍으로 실시간 결과 받기
긴 작업의 경우 결과가 다 나올 때까지 기다리는 것보다 중간 과정을 실시간으로 받는 게 더 좋을 때가 있습니다.
A2A는 message/stream 메서드를 통해 SSE(Server-Sent Events) 기반의 스트리밍을 지원합니다.
{
"jsonrpc": "2.0",
"id": 1,
"method": "message/stream",
"params": {
"message": {
"role": "user",
"parts": [{ "kind": "text", "text": "분기별 매출 보고서를 작성해줘" }],
"messageId": "msg-report-001"
}
}
}
요청을 보내면 SSE 이벤트들이 순차적으로 날아옵니다.
먼저 kind: "task" 이벤트로 작업이 등록되었음을 알려주고, 이어서 kind: "artifact-update" 이벤트들이 결과물의 조각을 하나씩 전달합니다.
각 아티팩트 업데이트에는 append 플래그가 있어서 이전 내용에 이어 붙일지, 새로 시작할지를 구분할 수 있고, lastChunk 플래그로 마지막 조각인지도 알 수 있습니다.
마지막으로 kind: "status-update" 이벤트가 completed 상태와 함께 도착하면 스트리밍이 끝납니다.
작업이 정말 오래 걸리는 경우에는 SSE 연결을 계속 유지하는 것보다 웹훅 방식이 더 효율적입니다. A2A는 푸시 알림(Push Notification)도 지원하는데요. 요청 시 웹훅 URL을 함께 보내면 에이전트가 작업 완료 후 해당 URL로 결과를 POST 합니다.
{
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{ "kind": "text", "text": "분기별 매출 보고서를 작성해줘" }],
"messageId": "msg-report-002"
},
"configuration": {
"pushNotificationConfig": {
"url": "https://my-app.example.com/webhook/a2a",
"token": "webhook-secret-token"
}
}
}
}
코드로 살펴보기
A2A는 Python, JavaScript/TypeScript, Go, Java, .NET 등 다양한 언어의 SDK를 제공합니다. TypeScript로 간단한 A2A 서버와 클라이언트를 만드는 예제를 살펴보겠습니다.
먼저 A2A 서버 쪽입니다.
import {
A2AServer,
InMemoryTaskStore,
TaskContext,
TaskYieldUpdate,
} from "@anthropic/a2a-sdk";
async function* handleTask(
context: TaskContext,
): AsyncGenerator<TaskYieldUpdate> {
// 작업 시작을 알림
yield {
state: "working",
message: {
role: "agent",
parts: [{ text: "이력서를 분석하고 있습니다..." }],
},
};
// 실제 분석 로직 수행
const result = await analyzeResume(context.task);
// 결과물 반환
yield {
name: "analysis-result",
parts: [{ text: JSON.stringify(result) }],
};
// 완료 상태로 전환
yield {
state: "completed",
message: {
role: "agent",
parts: [{ text: "분석이 완료되었습니다!" }],
},
};
}
const server = new A2AServer(handleTask, {
taskStore: new InMemoryTaskStore(),
});
server.start();
에이전트 로직을 AsyncGenerator로 작성하는 것이 핵심입니다.
yield로 상태 변경, 아티팩트, 완료 메시지를 순차적으로 내보내면 프레임워크가 알아서 A2A 응답으로 변환해줍니다.
클라이언트 쪽은 더 간단합니다.
import { A2AClient } from "@anthropic/a2a-sdk";
import { v4 as uuidv4 } from "uuid";
const client = new A2AClient("http://localhost:41241");
// 동기 요청
const result = await client.sendTask({
id: uuidv4(),
message: {
role: "user",
parts: [{ text: "이 이력서를 분석해줘" }],
},
});
console.log(result.artifacts);
스트리밍으로 받고 싶다면 sendTaskSubscribe를 사용합니다.
const stream = client.sendTaskSubscribe({
id: uuidv4(),
message: {
role: "user",
parts: [{ text: "상세한 분석 보고서를 작성해줘" }],
},
});
for await (const event of stream) {
if ("status" in event) {
console.log(`상태: ${event.status.state}`);
if (event.final) break;
} else if ("artifact" in event) {
console.log(`결과물: ${event.artifact.parts}`);
}
}
보안과 인증
기업 환경에서 에이전트 간 통신을 하려면 보안이 빠질 수 없습니다. A2A는 Agent Card에 보안 스킴을 선언하는 방식으로 인증을 처리합니다.
API 키, HTTP Bearer 토큰, OAuth 2.0, OpenID Connect, 상호 TLS(mTLS)까지 OpenAPI에서 지원하는 보안 스킴을 그대로 사용할 수 있어서 기존 인프라와의 통합이 수월합니다.
작업 도중에 추가 인증이 필요한 경우도 있습니다.
에이전트가 작업을 진행하다가 특정 리소스에 접근할 권한이 없으면, Task 상태를 auth-required로 전환해서 클라이언트에게 인증을 요청할 수 있습니다.
생태계와 현황
A2A는 2025년 4월에 초안이 공개된 후 Linux Foundation에 기부되었습니다. Google 혼자 만든 것이 아니라 Atlassian, Salesforce, SAP, ServiceNow, LangChain 등 50개 이상의 기업과 조직이 초기부터 참여하고 있습니다.
SDK는 pip install a2a-sdk로 Python용을 설치하거나, npm으로 JavaScript/TypeScript용을 가져올 수 있고, Go, Java, .NET 패키지도 제공됩니다.
모든 코드와 스펙은 GitHub 저장소에 오픈소스로 공개되어 있습니다.
마치며
A2A는 AI 에이전트 생태계에서 빠져 있던 퍼즐 조각을 채워넣는 프로토콜입니다. MCP가 에이전트에게 도구를 쥐어주는 역할이라면, A2A는 에이전트들이 팀으로 일할 수 있게 만드는 역할이죠. HTTP와 JSON-RPC라는 익숙한 기술 위에 만들어져서 기존 웹 인프라와 자연스럽게 어울린다는 점도 실용적입니다.
AI 에이전트를 만들고 있거나 여러 에이전트를 연동해야 하는 상황이라면, A2A 스펙을 한번 살펴보시는 걸 추천합니다.
더 자세한 내용은 A2A 공식 문서를 참고하세요.
This work is licensed under
CC BY 4.0