에이전틱 AI 재단(AAIF, Agentic AI Foundation)에 2026년 8월 17일 A2A(Agent2Agent)라는 프로젝트가 새로 들어왔습니다. 이름만 보면 에이전트끼리 통신하는 프로토콜이라는 것까지는 알겠는데, 공개된 스펙 외에 어떤 용도로 어떤 경우에 사용하면 좋을지를 자세히 알고 싶어졌습니다. 그래서 공식 SDK로 서버를 직접 실행해 보았습니다. 확인하려던 것은 3가지입니다. A2A는 어떤 경우에 쓰는 것인가, A2A를 쓰면 내 코드가 어떻게 달라지는가, 그리고 A2A 스펙이 약속한 것이 실제로 되기는 하는가입니다.

결과부터 말씀드리면 A2A로 얻는 것은 진행 확인과 취소의 규약입니다. 대신 응답 바이트가 늘어나고 호출당 지연 시간이 더 붙는데, 같은 작업에 응답이 순수 HTTP의 7배가 되고 지연은 1.2~2.1ms가 더 걸립니다. 다만 그 규약이 필요한 상황이 따로 있어서 짧은 단발 호출에서는 얻을 것이 없습니다. 그리고 에이전트 카드가 선언한 인증은 공식 SDK의 기본 구성에서 강제되지 않으니 선언과 강제를 나눠서 보셔야 합니다.

A2A가 어떤 프로젝트인지 먼저 정리하고 넘어가는 게 좋겠습니다. 구글이 2025년 4월에 공개했고 AWS, 시스코, 마이크로소프트, 세일즈포스, SAP, 서비스나우 등을 창립 조직으로 두고 리눅스 재단에 기부했습니다. 2026년 6월에 AAIF 프로젝트로 제안되어 기술위원회 승인과 이사회 승인을 거쳐 8월 17일에 호스티드 프로젝트가 되었고 재단 안의 단계는 Growth입니다(그 위가 Impact입니다). 스펙은 2026년 3월 12일에 v1.0.0이 고정되었고 이번에 기준으로 삼은 v1.0.1은 5월 28일 판으로 이 글을 쓰는 지금도 최신입니다. v1.0에서 메서드 이름과 파트 타입과 태스크 상태 이름이 바뀌었기 때문에, 0.x 시절에 나온 글의 필드 수준 설명은 지금 그대로 따라 하시면 맞지 않습니다.

[노트] 이 글에서 쓰는 용어
  • A2A: 에이전트끼리 일을 주고받는 방식을 정한 프로토콜입니다. 이 글에서 A2A라고만 쓰면 이 프로토콜을 가리킵니다.
  • A2A SDK: A2A를 구현한 공식 라이브러리입니다. 언어별로 있고 이 글에서 쓴 것은 파이썬판인 a2a-python입니다. 줄여서 SDK라고만 쓰기도 합니다.
  • 에이전트: 일을 넘기거나 받는 쪽의 서비스입니다. 넘기는 쪽을 클라이언트, 받는 쪽을 서버라고 부르기도 합니다.
  • 에이전트 카드(Agent Card): 에이전트가 자기를 설명하려고 공개해 두는 JSON 문서입니다. /.well-known/agent-card.json에 둡니다. 무엇을 할 수 있고 어디로 어떻게 부르는지, 어떤 인증을 요구하는지가 적혀 있습니다.
  • 인증 스킴(security scheme): 어떤 방식으로 인증할지를 가리키는 이름입니다. API 키, HTTP Bearer, OAuth2 같은 것들이고 에이전트 카드의 securitySchemes에 적습니다. 이름만 적어 두는 것이라 실제로 검사하는 일은 서버 몫입니다.
  • 메시지(Message): 에이전트끼리 주고받는 내용입니다. 일을 맡길 때도 중간에 되물을 때도 메시지로 합니다. 텍스트, 파일 그리고 구조화 데이터를 담을 수 있는데 그 하나하나를 파트(Part)라고 부릅니다.
  • 태스크(Task): A2A가 정한 일의 단위입니다. 메시지를 보내면 생기고 식별자와 상태, 이력 그리고 산출물을 가집니다. 쿠버네티스의 잡이나 CI 파이프라인에서 말하는 태스크와는 다른 것이니 구분해서 읽어 주시기 바랍니다.
  • 아티팩트(Artifact): 태스크가 만들어 낸 결과물입니다. 일이 끝나면 태스크에 담겨 남습니다. 중간에 오가는 메시지와 달리 이쪽이 최종 산출물입니다.

A2A가 정하는 것은 이 가운데 에이전트 카드, 메시지, 태스크 그리고 아티팩트 4가지, 그리고 그것을 실어 나르는 전송 규칙뿐입니다. 상대 에이전트가 쓰는 도구와 모델과 내부 계획은 밖에서 볼 수 없다고 가정하기 때문에, 상대 에이전트가 안에서 무엇을 어떻게 하는지는 A2A가 정하지 않습니다. 에이전트 카드를 읽고 메시지를 보내면 태스크가 생기고 일이 끝나면 아티팩트가 남는다는 것, 그리고 그 넷을 JSON-RPC와 HTTP+JSON(SDK에서는 REST라고 부릅니다)과 gRPC 3가지 방식 중 하나로 실어 보낸다는 것까지가 A2A의 범위입니다.

상대는 불투명하고 보이는 것은 4개뿐이다

여기까지 보시면 MCP(Model Context Protocol)와 무엇이 다른지 궁금하실 수 있습니다. 둘 다 JSON-RPC로 무언가를 호출하고 결과를 받는 규약이라 겉모습이 닮아 보이기 때문입니다. 둘은 상대편에 무엇이 있느냐가 다릅니다. 스펙 부록의 표현을 그대로 옮기면 MCP는 에이전트가 도구와 자원을 사용하는 방식을 표준화하고 A2A는 에이전트가 다른 에이전트에 일을 넘기는 방식을 표준화합니다. 도구는 호출하면 결과가 돌아오는 것이고 에이전트는 일을 받아서 시간을 들여 처리하다가 중간에 되물을 수도 있고 거절할 수도 있는 상대입니다. 그 결과 MCP에는 도구 목록과 도구 호출이 있고 A2A에는 에이전트 카드, 태스크 그리고 상태 변화가 있습니다. 여기서 한 가지 오해하기 쉬운 것이 둘의 관계인데, 순서대로 거쳐야 하는 것이 아니라 상황에 따라 고르는 것입니다. 도구를 직접 사용하려면 MCP만 있으면 되고 다른 에이전트에게 일을 넘길 때는 A2A를 사용하면 됩니다.

MCP와 A2A는 상대가 다르고 순서가 아니라 선택이다

측정은 3가지로 나누어 했습니다. 스펙이 약속한 것이 SDK 기본 구성에서 실제로 되는지를 항목 12개로 확인했고 같은 작업을 A2A, MCP 그리고 순수 HTTP 3가지로 만들어 클라이언트가 겪는 것을 비교했으며 프로토콜 자체가 지연과 바이트를 얼마나 더 쓰는지 부하를 걸어 측정했습니다. 쓴 버전은 스펙 v1.0.1과 a2a-python 1.1.2이고 대조군의 MCP 서버는 mcp 2.0.0, HTTP 서버는 표준 라이브러리로 만든 최소 JSON-RPC 서버입니다. 환경은 버추얼박스(VirtualBox) 3노드 쿠버네티스 1.37.0이라 절대 수치보다는 구현 사이의 상대 비교로 읽어 주시는 것이 좋겠습니다. 자세한 조건과 항목별 판정은 공개 저장소에 있습니다.

얻는 것은 진행 확인과 취소의 규약

먼저 A2A를 써서 얻는 것부터 보겠습니다. 같은 작업 함수를 3가지로 감싸서 15초짜리 일을 맡겨 보면, 결과를 받기까지 걸린 시간은 셋 다 15.1~15.2초로 같습니다. 다른 것은 그 15초 동안 클라이언트가 무엇을 할 수 있느냐입니다.

넘긴 일이 어디까지 됐는지 아는 방법은 3가지이고 A2A는 이 3가지를 모두 스펙에 두고 있습니다.

  • 폴링(polling): 클라이언트가 주기적으로 상태를 물어봅니다. 만들기는 가장 쉽지만 물어보는 횟수만큼 왕복이 생깁니다.
  • 스트리밍: 서버가 연결 1개를 열어 두고 상태가 바뀔 때마다 이벤트를 보냅니다. 클라이언트가 물어보러 갈 필요가 없습니다.
  • 푸시: 서버가 미리 등록해 둔 주소로 알림을 보냅니다. 연결을 붙들고 있지 않아도 되므로 오래 걸리는 일에 맞습니다. 다만 SDK 기본 구성에서는 설정 요청 자체가 거절되므로 쓰려면 배선이 필요한데 이 이야기는 뒤에서 하겠습니다.

15초 걸리는 일을 맡겨 놓고 무엇을 하는가

순수 HTTP와 MCP에도 같은 것을 만들 수는 있습니다. 다만 규약으로 주어지는 것이 없어서 전부 직접 만들어야 하고 이름도 응답 형식도 제가 정하는 것이라 상대 서버가 바뀌면 다시 정하게 됩니다. 이번 측정에서는 양쪽 다 상태를 조회하는 호출만 만들어 1초 간격으로 물어봤고 스트리밍과 푸시에 해당하는 것은 만들지 않았습니다. SSE 엔드포인트와 웹훅 발송 기능을 따로 붙여야 하기 때문입니다.

MCP에는 선택지가 하나 더 있는데 15초를 그대로 기다리는 블로킹 호출입니다. tools/call 1개를 보내고 결과가 올 때까지 붙들고 있으면 되니 상태 확인 왕복이 0회입니다. 대신 그동안 어디까지 됐는지 알 방법이 없고 타임아웃을 넘기면 결과를 잃습니다. mcp 2.0.0 코어에 태스크 수명주기가 없어서 나머지 선택지는 상태를 조회하는 도구를 따로 만드는 것뿐입니다.

취소도 마찬가지입니다. A2A는 취소가 스펙에 있고 SDK가 그것을 AgentExecutor(앱이 작업 로직을 넣는 클래스)의 취소 훅(hook)에 연결해 두었습니다. 진행 중인 태스크와 되물음을 기다리는 태스크는 실제로 취소되었습니다. 다만 완료된 태스크를 취소하면 거절되는데, 그 오류 코드가 스펙이 정한 TaskNotCancelableError(-32002)가 아니라 내부 오류(-32603)로 왔습니다. 취소 실패를 코드로 나누실 생각이라면 이 점을 감안하셔야 합니다. 순수 HTTP와 MCP에는 취소 규약이 아예 없어서 이번에는 표시만 바꾸는 최소 구현으로 두었고 그 구현에서는 작업 스레드가 그대로 계속 돌았습니다. 진짜 취소를 붙이는 것은 물론 가능하고 그 작업량이 규약이 없을 때 드는 값입니다.

한 가지 더 있는데 상대 에이전트가 중간에 되묻는 경우입니다. 어느 환경에 배포할지 같은 것을 처리 도중 물어야 할 때인데, A2A에는 그 상태(input-required)가 있어서 같은 태스크로 답을 이어 보내면 하던 일이 재개됩니다. MCP 코어에는 그 상태가 없어서 앱이 따로 만들지 않으면 호출을 처음부터 다시 하게 됩니다.

상대 에이전트가 늘어날 때 다시 만들지 않아도 되는 5가지

얻는 것을 숫자 하나로 보면 이렇습니다. 진행 확인과 취소를 만들려면 상대 에이전트에 대해 5가지를 알아야 합니다. 괄호 안은 A2A가 스펙으로 정해 둔 이름입니다.

  • 일을 시작하는 호출 이름 (message/send)
  • 그 일을 가리키는 식별자 필드 이름 (taskId)
  • 상태를 물어보는 호출 이름 (tasks/get)
  • 돌아오는 상태 값의 종류 (8종, submittedworkingcompleted 등)
  • 취소하는 방법 (tasks/cancel)

괄호 안은 v0.3 JSON-RPC 표기입니다. v1.0에서는 SendMessage, GetTask, CancelTask로 바뀌었고 상태 값도 completed에서 TASK_STATE_COMPLETED처럼 대문자에 접두가 붙는 형태가 되었습니다. 이름이 바뀌어도 스펙이 정해 준다는 것은 같습니다.

순수 HTTP와 MCP에서는 이 5가지를 에이전트마다 각자 정합니다. 그래서 붙을 상대 에이전트가 하나 늘 때마다 그쪽 규칙을 확인하고 클라이언트 코드를 새로 짜게 됩니다. A2A에서는 전부 스펙에 있으니 상대 에이전트가 바뀌어도 쓰던 코드가 그대로 붙습니다.

물론 사내 에이전트 1개만 호출한다면 이 숫자는 의미가 없습니다. 상대 에이전트가 하나면 그 하나에 맞추면 그만이고 규약을 따르느라 비용이 들 이유가 없습니다. 그런데 부를 곳이 5군데쯤 되거나 외부 업체가 운영하는 에이전트라면 그때마다 클라이언트를 새로 만들게 되고 그 양이 상대 에이전트 하나당 5가지입니다. 규약이 가치를 가지는 지점이 상대 에이전트의 수에 달려 있다는 뜻입니다.

A2A를 쓰면 늘어나는 응답 바이트와 지연 시간

얻는 것이 있으면 잃는 것도 있습니다. A2A를 쓰면 순수 HTTP로 만들었을 때보다 응답 바이트가 늘어나고 호출당 지연 시간도 조금 늘어납니다. 짧은 작업에서는 세 구현이 돌려준 결과 문자열이 글자까지 같았고 다른 것은 왕복 수, 지연 그리고 응답 바이트뿐이라 표로 정리해 볼 수 있습니다.

구현첫 호출 왕복지연 중앙값응답 바이트
HTTP1회3.2ms80
MCP1회5.9ms286
A2A2회6.1ms581

A2A의 첫 왕복은 에이전트 카드를 받아 오는 호출이고 858바이트가 듭니다. 응답이 큰 이유는 태스크 객체를 매번 함께 보내기 때문입니다. 태스크 객체에는 식별자와 상태와 이력이 들어 있는데, 나중에 진행을 확인하거나 취소할 때 쓰라고 미리 실어 주는 것입니다. 그런데 1초 안에 끝나는 호출에서는 그 나중을 쓸 일이 없습니다. 확인할 진행도 없고 취소할 일도 없으니 늘어난 바이트만큼을 쓰지 않고 버리는 셈입니다.

진행 확인을 폴링으로 하면 이 부담이 반복됩니다. 상태를 한 번 확인할 때마다 태스크 객체 전체가 실려 오기 때문에 A2A는 완료를 확인하는 마지막 응답이 571바이트인데(진행 중에 확인하면 423바이트입니다), 순수 HTTP 쪽에 직접 만든 상태 조회는 92바이트라 6배가 넘습니다. MCP는 그 중간인 286바이트입니다.

다만 이것은 폴링을 골랐을 때의 값입니다. 15초짜리 작업을 1초 간격으로 물어보면 완료까지 16번 확인하게 되고 그동안 6,916바이트를 받았습니다. 같은 일을 스트리밍으로 받으면 이벤트 4개를 합쳐 1,287바이트이고 푸시로 받으면 서버가 보낸 두 요청을 합쳐 465바이트입니다. 응답 1개가 무거운 것은 맞지만 오래 걸리는 일에서는 폴링을 버리는 쪽이 훨씬 적게 주고받고 끝납니다.

지연도 늘어나는데, 이것은 세 구현에 같은 부하를 걸어 확인했습니다. 초당 요청 수(rps, requests per second) 두 점과 연결 방식 2가지를 조합해 조건 4개를 만들고 구현 1개를 조건 1개에서 30초 돌린 것을 셀 1개로 셉니다. 회차를 10번 돌렸고 아래 표는 그 10개의 중앙값입니다.

조건HTTPMCPA2AA2A - HTTP
50rps, 매번 새 연결4.45.55.8+1.4
50rps, 연결 재사용3.45.55.5+2.1
100rps, 매번 새 연결3.64.44.8+1.2
100rps, 연결 재사용3.04.24.5+1.5

표의 값은 호출당 지연의 p50(요청 절반이 그 안에 끝나는 시간)이고 단위는 ms입니다. 초당 50건과 100건은 부하 생성기의 동시성이 8과 16으로 달라서, 행끼리 비교하지 않고 한 행 안에서 구현 사이의 차이만 읽으시면 됩니다.

여기서 A2A와 순수 HTTP의 차이(1.22.1ms)와 A2A와 MCP의 차이(00.4ms)를 함께 놓고 보면, 비용의 큰 부분이 A2A라서 붙는 것이 아니라 구조화된 프로토콜이라면 공통으로 붙는다는 것을 알 수 있습니다. 따라서 이미 MCP를 쓰고 있어서 그 정도 지연에 익숙하시다면 A2A를 더해도 지연이 크게 늘지는 않고 순수 HTTP만 쓰고 계셨다면 1.2~2.1ms가 그대로 붙습니다.

얻는 것과 잃는 것

선언만 되고 강제되지 않는 에이전트 카드의 인증

지금까지는 얻는 것과 잃는 것이었고 여기서부터는 믿으면 안 되는 것입니다. 스펙이 약속한 항목 12개 가운데 SDK 기본 구성이 스펙대로 동작한 것이 5개, 조건이 붙는 것이 4개, 다르게 동작한 것이 3개였습니다. 다르게 동작한 3개 중 2개는 원인이 같습니다.

먼저 인증입니다. 스펙 7.4절은 서버가 에이전트 카드에 선언한 요구에 따라 모든 요청을 인증하도록 정하고 있습니다. 그래서 에이전트 카드에 API 키와 HTTP Bearer와 OAuth2 3가지 인증 스킴을 한꺼번에 선언해 두었습니다. 셋 중 어느 것으로 막히는지, 그리고 틀린 키를 넣으면 어떤 오류가 오는지를 보려던 것이었습니다.

그런데 자격 증명 없이 그냥 호출해 보니 200이 돌아왔고 틀린 키를 실은 요청도 같았습니다. 막히는 방식을 비교하려고 만든 서버였는데 애초에 막히지가 않았습니다. 처음에는 서버 설정을 잘못한 줄 알고 에이전트 카드부터 다시 확인했습니다. 거기에는 스킴 3가지가 그대로 선언되어 있었습니다.

그리고 이 문제는 인증 하나로 끝나지 않았습니다. 확장 에이전트 카드(인증된 요청에만 주는 추가 정보 카드)도 마찬가지였습니다. 스펙은 인증된 요청에만 주도록 정하고 있지만 무인증 요청에도 공개 에이전트 카드에 없는 스킬(skill, 에이전트가 할 수 있는 일의 목록)까지 그대로 돌려주었습니다.

둘의 원인은 같은데, SDK의 기본 핸들러에 호출자가 누구인지 확인하는 부분이 아예 없기 때문입니다. 에이전트 카드의 securitySchemes는 클라이언트에게 이런 방식으로 인증해서 오라고 알려 주는 선언일 뿐입니다. 서버가 그 광고대로 검사하는 코드는 앱의 몫으로 남아 있습니다. 스펙이 서버 의무로 정해 두었는데도 기본 구성이 그것을 하지 않으니 간격이 생깁니다. 이전에 MCP 서버 앞에 게이트웨이를 두고 확인할 때도 정책이 수용된 것과 실제로 강제되는 것이 달랐는데, 이번에는 프로토콜 자체에서 같은 간격을 봤습니다.

따라서 에이전트 카드를 읽고 인증을 붙였다고 해서 서버가 막아 준다고 생각하시면 안 됩니다. 인증은 앱 안에서 직접 거시거나 앞단의 게이트웨이가 담당해야 합니다. 확장 에이전트 카드에 비공개 정보를 담는 설계도 같은 이유로 위험합니다. 신원을 확인하지 않으니 공개 에이전트 카드에서 빼고 확장 카드에만 둔 스킬이 아무에게나 나갑니다.

두 버전을 나누는 A2A-Version 헤더

다르게 동작한 나머지 1가지는 에이전트 카드가 광고하는 주소와 서버가 실제로 받는 주소가 어긋나는 것입니다. 그런데 그것을 읽으려면 서버가 두 세대(이 글에서는 v0.3과 v1.0을 세대라고 부르겠습니다)를 어떻게 나누는지부터 봐야 합니다. 세대 선택은 스펙에 분명히 적혀 있는데도 가장 알아채기 어려운 대목이었습니다.

A2A는 2026년 3월에 v1.0.0이 고정되면서 메서드 이름과 파트 타입과 태스크 상태 이름이 한 번에 바뀌었습니다. 이름이 바뀌었다고 기존 v0.3 클라이언트를 바로 끊을 수는 없으니 v0.3 호환을 켠 서버는 기존 v0.3과 새 v1.0을 함께 서비스하게 되는데, 어느 쪽으로 처리될지는 A2A-Version 헤더 값 하나로 나누어집니다. 스펙 3.6.1절이 클라이언트는 이 헤더를 반드시 보내라고 하면서 같은 문장에서 헤더가 없으면 v0.3으로 본다고 함께 적어 두었습니다.

그런데 문제는 v1.0 클라이언트가 헤더를 빠뜨렸을 때 생기는데, 오류를 돌려주는 것이 아니라 v0.3 요청으로 처리되어 버립니다. 저도 처음에는 “JSON-RPC는 v0.3 형식만 받는구나” 하고 잘못 판단했다가, 헤더를 붙여서 다시 호출해 보고 나서야 같은 주소가 v1.0 메서드 이름과 메시지 형식을 그대로 받는다는 것을 알게 되었습니다. 다른 세대로 처리된 결과가 정상 응답으로 오니 알아채기가 어렵습니다.

나누는 방식도 두 바인딩에서 서로 달랐습니다(gRPC는 처음부터 v1.0 스키마라 세대를 나누지 않습니다). JSON-RPC는 주소 하나에서 헤더로만 나뉘는데 REST는 경로까지 나뉘고 그마저 /a2a/rest/가 v1.0이고 /a2a/rest/v1/이 v0.3이라 이름과 실제가 반대로 보입니다(경로만 보고 v1.0을 고르면 v0.3으로 처리됩니다).

같은 주소가 헤더 하나로 두 세대로 나누어진다

이제 앞에서 말씀드린 나머지 1가지입니다. v0.3 호환을 끈 서버의 에이전트 카드에도 v0.3 인터페이스가 그대로 남아 있는데 그 주소를 실제로 호출하면 거절당합니다. 에이전트 카드가 광고하는 목록과 서버가 실제로 받는 주소가 따로 관리되기 때문인데, 앞 절의 인증과 함께 놓고 보면 에이전트 카드라는 문서를 어디까지 믿을지가 이 프로토콜에서 반복해서 나오는 물음이라는 것을 알 수 있습니다. 에이전트 카드만 읽고 접속하는 클라이언트는 있다고 적힌 곳에서 막히게 됩니다.

그래서 언제 쓰면 좋은가?

여기까지가 이번에 확인한 것입니다. 이제 도입을 언제 하면 좋을지를 얘기해 보겠습니다.

쓰지 않아도 되는 쪽부터 말씀드리면 사내 서비스 하나의 도구를 부르는 경우입니다. 그 자리에는 MCP면 충분하고 A2A를 얹어도 얻을 것이 없습니다. 같은 팀 에이전트에 1초 안에 끝나는 일을 넘기는 경우도 같습니다. 규약이 값을 할 일이 없고 상대가 1개뿐이라면 그쪽에 맞춰 순수 HTTP로 만드는 편이 효과적입니다.

반대로 의미를 가지는 곳이 2군데 있습니다. 먼저 오래 걸리는 일을 넘겨 놓고 진행을 보거나 중간에 취소해야 하는 경우입니다. 조회, 스트리밍, 푸시 그리고 취소가 스펙에 있어서 상대 에이전트가 바뀌어도 쓰던 클라이언트가 그대로 붙습니다. 다른 그리고 다른 팀이나 다른 조직의 에이전트를 여럿 호출하는 경우입니다. 에이전트 카드로 찾고 같은 규약으로 다루니 상대마다 다시 만들던 5가지가 사라집니다.

프로토타입은 따로 봐야 합니다. 일단 만들어 보고 판단하실 생각이라면 SDK 기본 구성만으로는 모자랍니다. 인증을 걸려면 호출자를 확인하는 코드를 앱에 직접 넣거나 앞단에 게이트웨이를 두어야 합니다. 푸시 알림을 쓰려면 설정을 담아 둘 곳과 보내는 부분을 만들어야 하고 재시작을 넘겨 태스크 상태를 유지하려면 저장소도 바꿔야 합니다. 이 셋을 붙이는 일이 프로토타입으로 확인하려던 것보다 커질 수 있습니다.

도입하신다면 확인할 점검 목록

끝으로 실제로 도입하실 때 확인하시면 좋을 내용을 정리해 보겠습니다.

  1. 에이전트 카드가 선언한 인증 스킴은 서버가 검사하지 않습니다. 인증은 앱이나 앞단에서 직접 거셔야 합니다.
  2. 확장 에이전트 카드에 비공개 정보를 담지 마시기 바랍니다. SDK 기본 구성은 신원을 확인하지 않아서 무인증 요청에도 그대로 돌려줍니다.
  3. A2A-Version 헤더는 반드시 붙이셔야 합니다. 빠뜨리면 오류 없이 v0.3으로 처리됩니다.
  4. 에이전트 카드의 인터페이스 목록을 그대로 믿지 마시고 실제로 호출해서 확인해 보시는 것이 좋습니다. v0.3 호환을 꺼도 에이전트 카드에는 그대로 남아 있습니다.
  5. 취소 실패를 오류 코드로 나누신다면 TaskNotCancelableError(-32002)만 보지 마시기 바랍니다. 완료된 태스크에서는 내부 오류(-32603)가 왔습니다.
  6. 푸시 알림을 쓰시려면 알림 설정을 담아 둘 곳과 푸시를 보내는 부분을 직접 만들어 연결하셔야 합니다. 연결하지 않으면 설정 요청 자체가 거절됩니다. 그리고 연결한 뒤에도 발송이 실패했을 때 태스크는 완료로 끝나기 때문에 클라이언트는 실패를 알 방법이 없고 서버 로그에만 남습니다.
  7. 진행 확인을 폴링으로만 하시면 상태 확인 한 번에 최대 571바이트를 받게 되니 스트리밍이나 푸시를 함께 검토해 보시는 것이 좋겠습니다.
  8. SDK 기본값은 태스크를 파드 메모리에 담는 InMemoryTaskStore라서 백엔드를 재시작하면 상태가 사라집니다. 재시작해도 남는 저장소로 바꾸셔야 합니다.

한계

이번 측정은 SDK 하나(파이썬 1.1.2)와 샘플 AgentExecutor를 바탕으로 만든 서버로 진행했기 때문에 다른 언어 SDK나 실제 서비스용 AgentExecutor에서는 다를 수 있습니다. 덧붙여 순수 HTTP와 MCP 쪽에 직접 만든 상태 조회는 비교를 위한 최소 구현이라, 실제 서비스라면 상태를 담아 둘 곳과 취소 전파와 알림을 더 붙이게 되고 그만큼 코드가 늘어납니다. 이 측정에서 만든 코드 양은 그 하한선이라고 보시면 되겠습니다.

한 가지 더 밝혀 둘 것이 있습니다. A2A는 2026년 3월에 v1.0이 나오면서 필드 이름이 바뀌었는데, 두 세대를 함께 재 보니 지연은 0~0.2ms 차이로 같았고 응답 바이트만 v1.0이 34바이트 작았습니다. 상태 이름은 길어졌지만 파트와 메시지의 타입을 가리키는 필드가 빠져서 총량이 줄어든 것입니다. 위 수치는 v0.3 형식 기준이고 v1.0으로 바꾸면 바이트가 그만큼 내려갑니다.

마치며

처음에 궁금했던 것은 3가지였습니다. 하나씩 답해 보겠습니다.

첫 번째, A2A는 어떤 경우에 쓰는 것인가?

이는 요청을 보내는 대상이 도구인지 에이전트인지에 따라 달라집니다. 부르면 결과가 돌아오는 도구라면 MCP로 충분합니다. 그런데 시간을 들여 일하다가 중간에 되묻기도 하고 거절하기도 하는 에이전트가 상대라면, 지금 어디까지 됐는지 묻고 필요하면 멈추는 방법이 있어야 합니다. 그 방법을 정해 둔 것이 A2A입니다.

두 번째, A2A를 쓰면 코드가 어떻게 달라지는가?

코드에서 달라지는 부분은 진행 확인과 취소입니다. 이 둘을 직접 만들지 않아도 됩니다. 시작 호출 이름부터 취소 규약까지 5가지를 스펙이 정해 두었기 때문에, 상대 에이전트가 하나 늘어도 쓰던 클라이언트가 그대로 붙습니다. 대신 응답 바이트가 늘어나고 호출당 1.2~2.1ms가 더 걸립니다. 상대가 1개뿐이고 호출이 짧다면 들 이유가 없는 비용입니다.

세 번째, A2A 스펙이 약속한 것이 실제로 되는가?

항목 12개 중 5개만 스펙대로였습니다. 4개는 조건이 붙었고 3개는 다르게 동작했습니다. 특히 에이전트 카드가 인증을 선언해도 SDK 기본 구성은 검사하지 않았습니다. 에이전트 카드에 적힌 것과 서버가 실제로 하는 것을 따로 보셔야 합니다.

측정에 사용한 스크립트와 매니페스트, 그리고 항목별 판정 표는 공개 저장소에 정리해 두었으니 직접 돌려 보고 싶으신 분들은 참고하시기 바랍니다. 다른 환경에서 재현해 보신 결과가 있다면 알려 주시면 좋겠습니다.

이번에는 같은 서버를 두 세대 형식으로 각각 호출해 봤습니다. 실제로 v0.3 클라이언트와 v1.0 서버가 섞였을 때 어떻게 되는지, 그리고 게이트웨이를 앞에 두면 세대를 어떻게 판정하고 에이전트 카드를 어떻게 고쳐 내보내는지는 이번 범위에 넣지 않았습니다.