A2A Protocol이 1.0이 되면서 바뀐 것들, 그리고 0.3 구현체를 옮기려면

(수정: 2026년 3월 29일) · 10분 읽기 Threads
목차

A2A Protocol v1.0.0 — 2026-03-12 릴리스. Google 주도 에이전트 간 통신 표준.
proto 파일이 정규 스펙으로 격상. gRPC/HTTP/JSON-RPC 멀티 바인딩 지원.

A2A Protocol v1.0 스펙 정리 커버 이미지

AI 에이전트들이 서로 대화하려면 공통 언어가 필요하다. MCP(Model Context Protocol)가 에이전트와 도구 사이의 연결을 담당한다면, A2A(Agent-to-Agent)는 에이전트와 에이전트 사이의 통신을 정의하는 프로토콜이다. Google이 주도하고 있으며, 2025년 4월 초안 발표 이후 약 1년 만에 v1.0에 도달했다.

v0.3까지의 A2A는 JSON-RPC(JSON 형식으로 원격 함수를 호출하는 프로토콜) 단일 바인딩에 의존했고, AgentCard 구조가 멀티 프로토콜 환경을 고려하지 않아 gRPC나 HTTP 바인딩을 추가하려면 스펙 외부에서 별도 처리가 필요했다. 1.0은 이 구조적 한계를 해결하면서, 동시에 프로토콜의 거의 모든 표면을 재설계했다. 이 글에서는 v1.0 스펙의 핵심을 정리하고, 0.3 기반 구현체를 운영하고 있다면 어떤 부분에서 변경이 발생하는지를 함께 다룬다. A2A의 기본 개념(AgentCard, Task, Part 등)은 이전 글에서 다루었으므로, 여기서는 변경사항에 집중한다.

A2A가 해결하는 문제

MCP(Model Context Protocol)는 에이전트가 외부 도구(API, DB, 파일시스템)를 호출하는 표준이다. A2A는 에이전트끼리 대화하는 표준이다. 둘은 경쟁이 아니라 보완 관계다.

하나의 에이전트가 모든 일을 처리하던 시대에서, 여러 에이전트가 역할을 나누어 협업하는 구조로 빠르게 이동하고 있다. 문제는 에이전트마다 프레임워크가 다르고, 내부 상태 관리 방식이 다르다는 것이다. 서로 다른 프레임워크로 만들어진 에이전트가 협업하려면 둘 다 이해하는 통신 규약이 필요하다.

A2A는 이 문제를 HTTP 기반의 표준 프로토콜로 해결한다. 에이전트가 자신의 능력을 AgentCard로 광고하고, 클라이언트가 이를 발견(discovery)하여 메시지를 주고받는 구조다. 내부 구현이 무엇이든 A2A 인터페이스만 맞추면 상호운용이 가능하다. A2A의 기본 개념을 더 자세히 알고 싶다면 이전 글을 참고하자.

v1.0 변경 요약

본격적인 스펙 설명에 앞서, 변경의 성격을 먼저 분류하면 읽기 수월하다.

분류변경 항목대응 난이도
네이밍 변경Enum(SCREAMING_SNAKE_CASE), Operation(SendMessage), 필드명(mediaType)검색/치환으로 기계적 대응
구조적 변경Part 통합, 스트림 이벤트 래퍼, 에러 형식파싱 로직 전면 수정
설계 변경AgentCard supportedInterfaces[], 멀티 바인딩, 버전 협상아키텍처 수준 검토

네이밍 변경은 양이 많지만 기계적이고, 구조적 변경은 코드 수정이 필요하며, 설계 변경은 디스커버리와 라우팅 로직의 재설계를 요구한다. 이 분류를 염두에 두고 각 항목을 살펴보자.

A2A v1.0 메시지 구조 변경

Enum, Part, Operation 이름 등 메시지의 표면적인 형태가 바뀐 부분이다. proto 파일이 정규 스펙으로 격상되면서, Protobuf(Protocol Buffers, Google이 만든 언어 중립적 직렬화 포맷)의 네이밍과 구조 규칙이 프로토콜 전체로 확산된 결과다. 이 변경들이 단순한 미학적 선택이 아닌 이유는, proto 기반 코드 생성을 했을 때 자연스럽게 올바른 구조가 나오도록 설계됐다는 점이다.

Enum 컨벤션

모든 Enum이 SCREAMING_SNAKE_CASE에 타입 접두사를 붙이는 형태로 통일됐다.

# v0.3
"submitted", "completed", "input-required"
"user", "agent"

# v1.0
"TASK_STATE_SUBMITTED", "TASK_STATE_COMPLETED", "TASK_STATE_INPUT_REQUIRED"
"ROLE_USER", "ROLE_AGENT"

JSON 직렬화할 때도 이 이름을 그대로 사용한다. Python SDK에서의 변경은 다음과 같다.

# v0.3
from a2a.types import TaskState
state = TaskState.completed        # lowercase
role = "user"                      # 문자열 직접 사용

# v1.0
from a2a.types import TaskState, Role
state = TaskState.TASK_STATE_COMPLETED   # SCREAMING_SNAKE_CASE
role = Role.ROLE_USER                    # Enum 타입으로 변경

Part 구조

0.3에서는 TextPart, FilePart, DataPart가 각각 독립된 타입이었다. v1.0에서는 하나의 통합 Part 구조체로 합쳐졌고, kind 판별자 대신 필드 존재 여부로 타입을 결정한다.

// v0.3 — 별도 타입, kind 판별자
{"kind": "text", "text": "안녕하세요"}
{"kind": "file", "file": {"url": "https://...", "mimeType": "image/png"}}

// v1.0 — 통합 Part, 필드 존재 여부로 판별
{"text": "안녕하세요"}
{"url": "https://...", "mediaType": "image/png"}
{"data": {"key": "value"}}

이 변경은 proto의 oneof 패턴과 직접 대응한다. SDK 코드에서의 차이를 보면 영향이 명확하다.

# v0.3 — 타입별 생성자
from a2a.types import TextPart, FilePart
parts = [TextPart(kind="text", text="Hello")]
# 판별: part.kind == "text"

# v1.0 — 통합 Part, 필드 존재로 판별
from a2a.types import Part
parts = [Part(text="Hello")]
# 판별: part.text is not None
# 파일: Part(url="https://...", media_type="image/png")

kind 기반 분기문이 모두 필드 존재 확인으로 바뀌어야 한다. file 중첩 객체도 평탄화됐고, mimeTypemediaType으로 이름이 바뀌었다.

Operations

Operation 이름이 슬래시 기반에서 PascalCase로 바뀌었다.

v0.3v1.0비고
message/sendSendMessage동기/비동기 선택 가능
message/streamSendStreamingMessageSSE(Server-Sent Events) 기반
tasks/getGetTask
tasks/cancelCancelTask
ListTasks1.0 신규

ListTasks는 v1.0에서 새로 추가된 Operation이다. 주목할 점은 페이지네이션 방식이 오프셋(page/perPage) 기반에서 커서(cursor/nextCursor) 기반으로 바뀌었다는 것이다. 기존에 오프셋 기반으로 Task 목록을 조회하던 구현체는 커서 토큰을 저장하고 전달하는 로직으로 전환해야 한다.

# v1.0 — 커서 기반 페이지네이션
response = await client.list_tasks(limit=20)
while response.next_cursor:
    response = await client.list_tasks(limit=20, cursor=response.next_cursor)

returnImmediately 옵션이 SendMessage에 추가됐다. true로 설정하면 서버가 즉시 Task 참조를 반환하고, 클라이언트는 이후 GetTask로 폴링하거나 웹훅으로 결과를 받는다. 기존에는 서버 구현에 따라 동기/비동기가 결정됐지만, 이제 클라이언트가 주도권을 갖는다. 다만 프로덕션에서 비동기 모드를 사용하려면 폴링 주기 설계, 타임아웃 정책, 클라이언트 재접속 처리 같은 운영 고려사항이 수반된다. 특히 네트워크 재전송으로 인한 중복 Task 생성을 방어하려면 Idempotency Key를 요청에 포함하는 설계가 필요하다.

A2A v1.0 스트리밍 이벤트 판별 변경

SSE 이벤트의 판별 방식이 kind 문자열에서 래퍼 객체로 바뀌었다.

// v0.3 — kind 필드로 판별
{"kind": "status-update", "taskId": "...", "state": "completed"}
{"kind": "artifact-update", "taskId": "...", "artifact": {...}}

// v1.0 — 래퍼 객체로 판별
{"taskStatusUpdate": {"taskId": "...", "state": "TASK_STATE_COMPLETED"}}
{"taskArtifactUpdate": {"taskId": "...", "artifact": {...}}}

final boolean도 제거됐다. 스트림이 닫히면 그것이 완료를 의미한다. Python SDK에서의 변경은 다음과 같다.

# v0.3 — kind 문자열로 분기, final로 완료 판별
async for event in stream:
    if event.kind == "status-update":
        handle_status(event.state)
    elif event.kind == "artifact-update":
        handle_artifact(event.artifact)
    if event.final:
        break

# v1.0 — 래퍼 키 존재로 분기, 스트림 종료가 곧 완료
async for event in stream:
    if event.task_status_update is not None:
        handle_status(event.task_status_update.state)
    elif event.task_artifact_update is not None:
        handle_artifact(event.task_artifact_update.artifact)
# 스트림이 닫히면 완료 — final 체크 불필요

SSE 이벤트를 파싱하는 클라이언트 코드에서 kind 기반 분기와 final 체크를 모두 제거해야 한다.

A2A v1.0 아키텍처 변경: proto 스펙, 멀티 바인딩

메시지 형태 변경과 별개로, 프로토콜의 구조적 토대가 바뀐 부분이다.

proto 파일이 정규 스펙이 된다

v1.0의 가장 근본적인 변화다. 0.3에서 a2a.proto는 gRPC 바인딩을 위한 구현 파일 중 하나였지만, 1.0에서는 프로토콜의 정규 스펙(normative specification)이 됐다. JSON Schema나 문서가 아닌 proto 파일이 진실의 원천(source of truth)이다.

이 결정에는 트레이드오프가 있다. proto 기반 코드 생성으로 타입 안전성과 멀티 언어 지원을 자동으로 얻는 대신, Protobuf 툴체인(protoc, 언어별 플러그인)에 대한 의존이 생긴다. Protobuf를 처음 접하는 팀이라면 빌드 파이프라인 통합에 학습 곡선이 있고, 바이너리 직렬화 포맷은 JSON에 비해 디버깅이 불편하다. 또한 proto 스키마는 진화 규칙(필드 번호 유지, 타입 변경 불가 등)을 엄격히 따라야 하므로, 스키마 변경 계획을 사전에 수립하는 것이 중요하다.

다만 JSON 바인딩도 1급으로 지원되므로, proto를 꼭 사용해야 하는 것은 아니다. 팀 규모가 작고(5인 이하) 단일 언어로 운영하며 외부 에이전트 연동이 없다면, JSON-only 전략이 더 현실적이다. proto 도입은 다국어 팀이나 외부 파트너와의 상호운용이 필요할 때 비용 대비 효과가 분명해진다.

멀티 바인딩

세 가지 프로토콜 바인딩이 모두 1급 시민으로 지원된다.

바인딩전송스트리밍적합한 환경
JSON+HTTPHTTP POSTSSE웹, 범용. 디버깅 용이
gRPC(Google이 만든 고성능 RPC 프레임워크)HTTP/2gRPC stream마이크로서비스 내부. 타입 안전성, 성능 이점. HTTP/2 필수
JSON-RPCHTTP POST폴링/웹훅레거시 호환. 0.3 기존 구현과 호환

대부분의 경우 하나의 바인딩만 선택하면 된다. 디폴트는 single binding이다. JSON+HTTP가 가장 범용적이고 디버깅도 쉬워서, 특별한 이유가 없다면 여기서 시작하는 것을 권장한다. 멀티 바인딩이 정당화되는 경우는 제한적이다. 내부 마이크로서비스 간 고빈도 통신에서 gRPC의 강타입과 스트리밍 성능이 필요하거나, 기존 0.3 JSON-RPC 구현체와의 하위 호환을 유지하면서 새 바인딩을 추가해야 하는 상황 정도다.

멀티 바인딩을 동시에 운영하면 복잡도가 올라간다. 바인딩마다 모니터링 포인트가 늘고(로그 포맷, 메트릭 태그 분리), rate limiting과 인증 정책을 이중으로 관리해야 하며, 장애 시 디버깅 표면적이 넓어진다. 운영팀 규모와 observability 성숙도를 먼저 따져야 한다.

AgentCard 재설계

에이전트의 “명함”인 AgentCard에서 가장 큰 변화는 supportedInterfaces[] 배열의 도입이다.

// v0.3
{
  "name": "My Agent",
  "url": "https://agent.example.com/a2a",
  "protocolVersion": "0.3.0",
  "preferredTransport": "JSONRPC",
  "capabilities": {"streaming": true}
}

// v1.0
{
  "name": "My Agent",
  "supportedInterfaces": [
    {
      "url": "https://agent.example.com/a2a",
      "protocolBinding": "jsonrpc+http",
      "protocolVersion": "1.0"
    },
    {
      "url": "https://agent.example.com/grpc",
      "protocolBinding": "grpc",
      "protocolVersion": "1.0"
    }
  ],
  "capabilities": {"extendedAgentCard": true}
}

최상위에 있던 url, protocolVersion, preferredTransport가 사라지고, supportedInterfaces 배열 안에 인터페이스별로 들어갔다. 하나의 에이전트가 JSON-RPC와 gRPC를 동시에 지원하거나, v0.3과 v1.0 인터페이스를 함께 광고할 수 있다. 이것이 점진적 마이그레이션의 핵심 메커니즘이다.

Discovery 경로는 .well-known/agent-card.json으로 동일하다. 클라이언트는 A2A-Version 헤더로 원하는 버전을 협상한다.

에러 처리

에러 형식이 RFC 9457(Problem Details)에서 Google의 google.rpc.Status로 바뀌었다.

항목v0.3v1.0
형식RFC 9457 Problem Detailsgoogle.rpc.Status + ErrorInfo
Content-Typeapplication/problem+jsonapplication/json

google.rpc.Status는 구조화된 에러 정보(에러 코드, 상세 원인, 메타데이터)를 체계적으로 전달할 수 있지만, RFC 9457에 비해 응답이 장황해져 별도 도구 없이 빠르게 디버깅하기에는 불편할 수 있다.

인증

OAuth 2.0 기반은 유지되지만, 플로우 선택지가 달라졌다. ImplicitOAuthFlowPasswordOAuthFlow가 deprecated되고, DeviceCodeOAuthFlow(RFC 8628)가 추가됐다. CLI나 IoT 디바이스처럼 브라우저가 없는 환경에서 에이전트 인증이 가능해졌다. 외부 에이전트와 연동할 때 상호 신뢰를 확인하는 수단으로, pkce_required 필드도 추가돼 PKCE 강제 여부를 AgentCard에서 선언할 수 있다.

flowchart TD
    subgraph 기존 인프라 재활용
        LB[Load Balancer]
        GW[API Gateway]
        OBS[Observability<br>Prometheus / OTel]
    end
    subgraph A2A Agents
        A1[Agent A<br>JSON+HTTP]
        A2[Agent B<br>gRPC]
        A3[Agent C<br>JSON-RPC]
    end
    LB --> GW
    GW --> A1
    GW --> A2
    GW --> A3
    OBS -.->|메트릭 수집| GW

아키텍처 철학은 Stateless, Layered Architecture를 지향한다. 기존 웹 인프라(로드밸런서, API Gateway, 관측성 도구)를 에이전트 시스템에 그대로 적용할 수 있다.

More: 신규 기능

v1.0에서 추가된 기능 중 실무적으로 의미 있는 것들이다.

Multi-Tenancy — 모든 요청에 tenant 필드가 추가됐다. 하나의 A2A 엔드포인트에서 여러 테넌트의 에이전트를 호스팅할 수 있다. SaaS 형태로 에이전트 서비스를 제공하는 시나리오에서 필수적인 기능이다.

Agent Card 서명 — JWS(JSON Web Signature)와 JSON Canonicalization(RFC 8785)을 사용해 AgentCard의 무결성을 암호학적으로 검증할 수 있다. 신뢰할 수 없는 외부 에이전트와 협업할 때 중간자 변조를 방지하는 수단이다. 다만 서명/검증 로직 구현과 키 관리 인프라가 추가로 필요하다.

버전 협상A2A-Version 헤더로 클라이언트와 서버가 프로토콜 버전을 협상한다. AgentCard에서 v0.3과 v1.0 인터페이스를 동시에 광고할 수 있어, 점진적 마이그레이션이 가능하다.

타임스탬프 — Task 객체에 createdAt/lastModified 필드가 추가됐다. ISO 8601 UTC 밀리초 정밀도. 디버깅과 감사 로그에 유용하다.

이 기능들은 모두 선택적이다. 초기 단계에서 권장하는 기본값은 JSON+HTTP 단일 바인딩, 단일 테넌트, AgentCard 서명 미도입이다. Multi-Tenancy는 실제로 여러 고객에게 에이전트를 SaaS로 제공할 때, AgentCard 서명은 신뢰할 수 없는 외부 에이전트와 연동할 때 도입을 검토하면 된다. 필요하지 않은 기능을 미리 구현하면 운영 복잡도만 올라간다.

0.3 구현체가 있다면: 마이그레이션 가이드

여기서부터는 이미 v0.3 기반으로 A2A 서버나 클라이언트를 운영하고 있는 경우의 이야기다. A2A를 네이티브 지원하지 않는 플랫폼 앞에 엣지 서버를 Python SDK 기반으로 두고 운영하던 구현체를 기준으로, 1.0 전환 시 필요한 작업을 정리했다.

마이그레이션 체크리스트

실제 전환 시 확인해야 할 항목을 단계별로 정리했다.

1단계: SDK 업그레이드 + 타입 스캔

2026년 3월 기준, Python a2a-sdk의 PyPI 최신 버전은 0.3.25다. Java SDK(a2a-java-sdk)도 1.0.0.Alpha1 단계다. SDK 1.0이 아직 출시되지 않았으므로, 두 가지 경로가 있다.

  • 경로 A (권장): v1.0 스펙을 먼저 숙지하고, SDK 릴리스 후 적용한다. 아래 체크리스트로 변경 범위를 사전 파악해둔다
  • 경로 B: GitHub 소스에서 직접 설치하여 선행 테스트한다 (pip install git+https://github.com/a2aproject/a2a-python-sdk@main)

SDK 1.0이 릴리스되면 다음과 같이 업그레이드한다.

# pyproject.toml 의존성 업데이트
# "a2a-sdk>=0.3.10" → "a2a-sdk>=1.0.0"

pip install -U "a2a-sdk>=1.0.0"
mypy --strict src/    # Python 정적 타입 체커로 변경점 자동 식별

타입 체커가 잡아주는 변경이 대부분이다. 나머지는 아래 항목을 하나씩 확인한다.

2단계: Part 파싱 로직 전환

가장 영향이 큰 구조적 변경이다. kind 기반 분기가 모두 필드 존재 확인으로 바뀌어야 한다.

# v0.3 — kind 기반 분기
def process_part(part):
    if part.kind == "text":
        return part.text
    elif part.kind == "file":
        return download(part.file.url)

# v1.0 — 필드 존재 확인
def process_part(part: Part):
    if part.text is not None:
        return part.text
    elif part.url is not None:
        return download(part.url)    # file 중첩 제거, 평탄화
    elif part.data is not None:
        return process_data(part.data)

3단계: Enum 상수 업데이트

# v0.3
TaskState.completed → TaskState.TASK_STATE_COMPLETED
TaskState.failed    → TaskState.TASK_STATE_FAILED
TaskState.canceled  → TaskState.TASK_STATE_CANCELED

# 이벤트에서도 동일
"status-update""taskStatusUpdate" (래퍼 키)
"artifact-update""taskArtifactUpdate" (래퍼 키)

4단계: AgentCard 재구성

# v0.3
AgentCard(
    url="https://example.com/a2a",
    protocol_version="0.3.0",
    preferred_transport="JSONRPC",
)

# v1.0
AgentCard(
    supported_interfaces=[
        AgentInterface(
            url="https://example.com/a2a",
            protocol_binding="jsonrpc+http",
            protocol_version="1.0",
        )
    ],
)

5단계: 에러 응답 포맷 변환

# v0.3 — RFC 9457 Problem Details
{"type": "about:blank", "title": "Unauthorized", "status": 401}

# v1.0 — google.rpc.Status
{"code": 16, "message": "Unauthorized", "details": [
    {"@type": "type.googleapis.com/google.rpc.ErrorInfo",
     "reason": "AUTH_REQUIRED"}
]}

6단계: (선택) 버전 협상 설정

모든 클라이언트가 동시에 전환할 수 없다면, AgentCard에 두 버전을 동시 광고한다.

{
  "supportedInterfaces": [
    {"url": "/a2a", "protocolBinding": "jsonrpc+http", "protocolVersion": "0.3.0"},
    {"url": "/v1/a2a", "protocolBinding": "jsonrpc+http", "protocolVersion": "1.0"}
  ]
}

서버에서 경로별로 v0.3/v1.0 핸들러를 분리하면 점진적 전환이 가능하다. 클라이언트는 요청 시 A2A-Version: 1.0 헤더를 보내 원하는 프로토콜 버전을 명시한다. 이 헤더가 없으면 서버는 v0.3으로 간주하므로, 1.0 클라이언트라면 반드시 헤더를 포함해야 한다.

실서비스에서는 롤링 배포 시 양 버전이 공존하는 기간에 주의가 필요하다. Task 저장소의 스키마(Enum 값, Part 구조), 이벤트 로그 포맷, 클라이언트-서버 간 프로토콜 버전 mismatch를 사전에 점검해야 한다. canary 배포로 일부 트래픽만 1.0으로 전환하면서 모니터링하는 것을 권장한다. 이중 스키마 유지 기간은 6개월 이내로 제한하고 sunset date를 명시하는 것이 좋다. 장기간 공존을 허용하면 분석 파이프라인 복잡도와 팀 온보딩 비용이 누적된다.

마이그레이션 요약 체크리스트

단계작업난이도주요 함정
1SDK 업그레이드 + 타입 스캔낮음SDK 1.0 미출시 시 경로 A/B 선택 필요
2Part 파싱 로직 전환높음kind 분기 → 필드 존재 확인, file 중첩 평탄화 누락
3Enum 상수 업데이트낮음JSON 직렬화에서도 SCREAMING_SNAKE_CASE 사용
4AgentCard 재구성중간supportedInterfaces[] 배열 구조, url 위치 변경
5에러 응답 포맷 변환중간google.rpc.Status 코드 매핑, 클라이언트 파싱 로직
6버전 협상 설정중간A2A-Version 헤더 누락 시 v0.3 폴백, sunset date 설정

A2A v1.0은 2026년 3월에 릴리스된 첫 메이저 버전이다. backward compatibility 정책이나 deprecation 일정은 공식 스펙에서 확인할 수 있다. SDK 생태계(Python, Java 등)가 아직 1.0에 도달하지 않은 경우도 있으므로, 실 적용 전 SDK 버전을 반드시 확인한다.

정리

이미 0.3 기반 구현체를 운영하고 있다면, 지금 당장 해야 할 일은 SDK를 1.0으로 올려보고 mypy/tsc로 breaking point를 확인하는 것이다. 전면 전환이 부담되면 AgentCard에 두 버전을 동시 광고하고, 클라이언트가 모두 전환된 시점에 0.3 경로를 제거하면 된다.

  • proto 중심 스펙: a2a.proto가 진실의 원천. 코드 생성 기반 접근이 유리하지만, JSON 바인딩만으로도 충분하다
  • 멀티 바인딩: JSON+HTTP, gRPC, JSON-RPC 모두 1급 지원. 환경과 운영 역량에 맞게 선택한다
  • 버전 협상: AgentCard에서 여러 버전을 동시 광고. 급한 마이그레이션은 불필요하다
  • 기존 인프라 활용: Stateless, Layered Architecture 지향. LB, Gateway, 관측성 도구를 그대로 쓸 수 있다
  • 마이그레이션 핵심: Part 구조 평탄화와 Enum 네이밍이 가장 영향이 크고, SDK 기반이면 타입 체커가 대부분 잡아준다

MCP가 에이전트의 “손”이라면, A2A는 에이전트의 “입”이다. 멀티 에이전트 시스템을 설계하고 있다면, 이 두 프로토콜의 역할 분담을 이해하는 것이 출발점이 될 것이다.

이어서 읽기