AI 에이전트 두 개를 붙이면 뭔가 마법처럼 알아서 협업할 것 같지? 현실은 좀 더 촌철살인이다. 대화는 잘하는데 일은 못 넘기고, 넘기기는 했는데 ack가 없고, ack는 왔는데 상태가 안 맞고, 재시도는 했는데 중복이 터지고, 체크는 있다고 믿었는데 아무도 확인 안 한 상태가 금방 온다.
그래서 A2A 프로토콜은 “에이전트끼리 채팅하는 규격”이 아니라, 서로 다른 프레임워크와 서버 위에 있는 에이전트가 작업을 위임하고, 상태를 추적하고, 결과를 회수하는 공용 언어로 봐야 한다. 공식 README도 A2A를 opaque agentic applications 사이의 통신과 상호운용성을 위한 오픈 프로토콜로 설명하고, 핵심 개념으로 AgentCard, Task, Artifact, streaming, push notifications를 잡는다. README
한 줄 요약: A2A는 에이전트에게 “말해라”가 아니라 “발견하고, 넘기고, 확인하고, 실패를 복구하라”를 가르치는 프로토콜이다. 진짜 병목은 모델 성능보다
handoff / ack / retry / check설계에 있다.
먼저 결론
- A2A는 MCP의 대체재가 아니다. MCP가 주로 에이전트의 도구 접근을 정리한다면, A2A는 에이전트 대 에이전트 협업을 정리한다.
- A2A의 핵심 단위는 메시지가 아니라 Task다. 단순 질의는 바로 답할 수 있지만, 실제 협업은 대부분 상태를 가진 Task로 흘러간다.
- 스트리밍은 예쁜 옵션이 아니라 운영 차이다. 실시간 업데이트가 있으면 사람이 덜 불안하고, 에이전트도 중간 실패를 더 빨리 회수한다.
- push notification은 편하지만 idempotency가 없으면 사고 난다. 같은 알림이 두 번 와도 시스템이 멀쩡해야 한다.
- check는 사치가 아니라 안전장치다. A2A는 “보냈다”로 끝나는 게 아니라, 누가 언제 무엇을 확인했는지 남겨야 덜 터진다.
A2A가 뭔데 이렇게 호들갑이냐
공식 문서에서 A2A는 서로 다른 프레임워크로 만들어진 에이전트가, 서로 내부 상태나 툴을 다 까지지 않고도 협업할 수 있게 하려는 표준이다. 즉 에이전트의 속살은 숨기고, 협업 인터페이스만 맞추는 방식이다.
이게 왜 중요하냐면, 에이전트가 하나일 때는 대충 프롬프트만 잘 써도 돌아간다. 그런데 둘 이상 붙는 순간부터는 다음 문제가 바로 튀어나온다.
- 이 에이전트가 뭘 할 수 있는지 어떻게 알지?
- 지금 이 작업이 끝났는지, 진행 중인지, 멈췄는지 어떻게 알지?
- 결과가 바로 오지 않으면 어디서 기다려야 하지?
- 알림이 두 번 오면 어떻게 하지?
- 중간에 인증이 필요하면 누가 책임지지?
A2A는 이 질문들에 대해, 발견 – 협상 – 수행 – 추적 – 회수의 공용 구조를 준다. 공식 사양은 JSON-RPC 2.0 기반으로, Task가 상태를 가지며 Send Message, Send Streaming Message, Get Task, Subscribe to Task, push notification 설정 같은 흐름을 정의한다. Specification
어디서 막히는가: 실전 병목 5개
1. 발견 단계에서 막힌다: AgentCard가 부실하면 시작도 못 한다
A2A에서 클라이언트는 먼저 상대의 AgentCard를 보고 무엇을 할 수 있는지, 어디로 붙는지, 인증은 어떻게 하는지 읽어야 한다. 그런데 여기서부터 자주 깨진다.
- capability는 적어놨는데 실제 지원 안 함
- endpoint는 있는데 auth 요구사항이 빠짐
- 텍스트만 지원하는데 파일/구조화 데이터를 기대함
- 버전이나 프로토콜 범위가 불명확함
즉, 첫 번째 장애물은 모델이 아니라 메타데이터 품질이다. 에이전트가 똑똑해도 카드가 허술하면 협업은 시작 안 된다.
2. handoff에서 막힌다: 보냈다고 전달된 게 아니다
사람들은 종종 “메시지를 넘겼다”를 “일이 넘어갔다”와 같은 뜻으로 쓴다. A2A에서는 이 둘을 분리해야 한다.
- 메시지를 보냈다
- Task가 생성됐다
- 상대가 그 Task를 수락했다
- 실제 작업이 시작됐다
- 중간 상태가 갱신됐다
- 결과 Artifact가 나왔다
이 중 어디까지 왔는지 안 적으면, 팀은 금방 “어? 저쪽이 받았나?” 모드가 된다. 그래서 handoff에는 반드시 상태명과 확인 주체가 필요하다.
3. ack에서 막힌다: 2xx가 왔는데 사람은 안 믿는다
push notification이나 webhook 기반 흐름에서는 수신측이 HTTP 2xx로 수신 확인을 돌려줘야 한다. 공식 사양도 클라이언트가 성공 수신에 응답하고, 중복 전달 가능성을 고려해서 idempotent하게 처리하라고 적는다. Authentication & Authorization
여기서 흔한 실수는 이거다.
- 응답은 200인데 내부 큐엔 안 넣음
- 큐엔 넣었는데 ack를 늦게 줌
- ack를 줬는데 재시도 방어가 없음
- 같은 notification을 두 번 받았을 때 중복 실행됨
결국 ack는 “응답 코드”가 아니라 업무적으로 받았다는 약속이어야 한다.
4. retry에서 막힌다: 재시도는 따뜻한 위로가 아니라 중복의 문
공식 사양은 실패한 delivery에 대해 exponential backoff 재시도를 권장한다. 이 말은 멋있는데, 구현은 안 멋있다. 왜냐면 retry는 곧 중복 실행 가능성이기 때문이다.
그래서 retry는 아래 조건이 없으면 독이 된다.
- idempotency key
- taskId 기반 dedupe
- attempt count 상한
- terminal state 확인
- 실패 시점의 체크포인트
재시도는 “다시 해보자”가 아니라 “같은 일을 두 번 해도 결과가 같게 만들자”에 가깝다.
5. check에서 막힌다: 누가 봤는지 기록이 없으면 그냥 미확인이다
A2A는 task 상태를 추적할 수 있게 해주지만, 실제 운영에서 중요한 건 누가 무엇을 언제 확인했는지다. 이 check가 없으면 다음 상황이 발생한다.
- 작업은 끝났는데 누구도 안 읽음
- 실패는 났는데 담당자가 못 봄
- 인증이 필요한데 다음 담당자로 안 넘김
- streaming이 끊겼는데 최종 상태를 안 가져옴
즉, check는 단순 조회가 아니라 운영 책임의 끈이다.
A2A가 잘 굴러가는 handoff 패턴
flowchart LR
A[Client Agent] --> B[AgentCard 조회]
B --> C{Capability / Auth OK?}
C -->|No| X[다른 에이전트 선택]
C -->|Yes| D[Send Message / Send Streaming Message]
D --> E[Task 생성]
E --> F[status update / artifact update]
F --> G{Need input?}
G -->|Yes| H[input_required or auth_required]
G -->|No| I[completed / failed / canceled / rejected]
H --> J[Client ack / check / resume]
I --> K[Get Task / Subscribe / Push confirm]
이 흐름에서 중요한 건 “전송”보다 상태 전환의 책임선이다.
- Client Agent는 요청을 넣고 확인한다
- Remote Agent는 Task 상태를 관리한다
- Notification 수신자는 ack를 한다
- Retry 담당자는 중복을 막는다
- Check 담당자는 누락을 잡는다
이 다섯 개가 한 문서에 안 적혀 있으면, 나중엔 다들 서로 “그거 네가 볼 거 아니었어?”만 반복한다. 개발판 단톡방의 무한 루프다.
실전 체크리스트
붙이기 전에
- AgentCard에 capabilities, endpoint, auth 요구사항을 명시했는가
- 이 협업이 단발 질의인지, Task 기반 장기 작업인지 정했는가
- streaming / polling / push 중 무엇을 쓸지 정했는가
- taskId와 contextId를 어디에 저장할지 정했는가
넘길 때
- handoff 메시지에 목표, 입력, 금지사항을 넣었는가
- ack를 받을 주체가 명확한가
- 중간 실패 시 누가 retry를 책임지는가
- terminal state 정의가 있는가
받았을 때
- 수신이 idempotent한가
- 중복 notification을 걸러낼 키가 있는가
- auth_required / input_required 상태에서 다음 액션이 정의돼 있는가
- 최종 결과를 Get Task나 Subscribe로 재확인하는가
끝난 뒤
- Artifact를 저장했는가
- 어떤 check가 성공/실패했는지 기록했는가
- 실패 케이스를 다음 AgentCard / prompt / routing 규칙에 반영했는가
초간단 예시: 사람이 아니라 Task를 넘겨야 할 때
예를 들어, 하나의 에이전트는 리서치를 잘하고 다른 에이전트는 요약을 잘한다고 치자. 이때 좋은 handoff는 이런 식이다.
- 리서치 에이전트가
Task를 만들고 핵심 자료를 모은다. - 결과를
Artifact로 내보낸다. - 요약 에이전트는
Task상태와 Artifact를 읽는다. - 요약이 끝나면 클라이언트는
Get Task또는Subscribe to Task로 최종 상태를 다시 확인한다. - push notification이 왔다면 2xx로 ack하고, 같은 알림은 두 번 와도 한 번만 처리한다.
이게 단순해 보이지만, 실제 팀에서는 이 다섯 단계 중 하나가 흔히 사라진다. 사라진 한 단계가 나중에 버그 티켓 세 장으로 돌아온다. 참 성실한 녀석이다.
MCP랑 같이 보면 더 정확하다
A2A를 이해할 때 많이 헷갈리는 지점이 있다. “그럼 MCP 있으면 되지 않나?”라는 질문이다.
내 해석은 이렇다.
- MCP는 에이전트가 도구와 데이터에 접근하는 방식을 정리한다.
- A2A는 에이전트가 다른 에이전트와 협업하는 방식을 정리한다.
그래서 둘은 경쟁 관계라기보다 레이어가 다르다. 도구를 잘 다루는 것과, 다른 에이전트에게 일을 잘 넘기는 건 같은 문제가 아니다. 솔직히 말하면 후자가 더 골치 아프다.
마무리
A2A의 진짜 포인트는 “에이전트를 연결할 수 있다”가 아니다. 연결한 다음에 어디서 막히는지 예측할 수 있다는 데 있다.
초기에는 다들 프로토콜의 이름만 보고 표준의 승리를 기대한다. 그런데 운영에 들어가면 이름보다 더 중요한 게 보인다.
- discovery가 되느냐
- handoff가 명확하냐
- ack가 신뢰되느냐
- retry가 중복을 만들지 않느냐
- check가 책임을 남기느냐
여기까지 설계돼야 비로소 “에이전트끼리 붙였다”고 말할 수 있다.
그리고 그때부터 진짜 재미가 시작된다. 물론 재미만 있고 사고가 없는 건 아니고, 보통 둘은 세트다. 인생이 원래 좀 그렇다.