첫 A2A 에이전트 구현하기
개념을 코드 구조로 바꾸기
앞 장까지 A2A를 Agent Card, Message, Task, Artifact의 관점에서 살펴보았다. 이제 필요한 질문은 단순하다. 이 개념들을 실제 프로젝트에서는 어디에 놓아야 할까?
첫 A2A 에이전트를 만들 때 목표를 크게 잡을 필요는 없다. 처음부터 복잡한 추론, 여러 도구, 장시간 워크플로, 푸시 알림을 모두 구현하려 하면 프로토콜의 기본 흐름이 흐려진다. 시작점은 작게 잡는 편이 좋다. 원격 에이전트가 자신을 Agent Card로 소개하고, 클라이언트가 그 카드를 읽고, 사용자의 메시지를 보내고, 서버가 그 메시지를 하나의 Task 또는 응답 Message로 처리해 결과를 돌려주는 구조면 충분하다.
즉 첫 구현의 핵심은 모델 성능이 아니라 연결 구조다. A2A 서버는 자신이 어떤 일을 할 수 있는지 공개한다. A2A 클라이언트는 그 정보를 읽고 호출 방법을 결정한다. 요청이 들어오면 서버는 내부 에이전트 로직을 실행하되, 외부에는 A2A 객체로 상태와 결과를 표현한다.
AI 생성 이미지
어떤 SDK로 시작할까
A2A 프로젝트는 Python, JavaScript, Java, C#/.NET, Go, Rust SDK를 제공한다. 공식 SDK 개요는 각 언어별 저장소와 샘플 저장소를 함께 안내한다.1 언어 선택은 프로토콜 자체보다 운영 환경에 맞추는 것이 좋다.
Python은 에이전트 실험과 빠른 프로토타이핑에 적합하다. LLM 라이브러리, 데이터 처리 도구, 예제 코드가 풍부하기 때문이다. JavaScript 또는 TypeScript는 웹 서비스, 프런트엔드 연계, Node.js 기반 백엔드에 자연스럽다. Java와 C#/.NET은 기존 엔터프라이즈 시스템, 강한 타입, 조직 표준 개발 프로세스와 잘 맞는다. Go는 단순한 배포, 높은 동시성, 작은 런타임을 선호하는 팀에 유리하다. Rust는 성능과 메모리 안전성을 중시하는 시스템에 적합하지만, 팀의 숙련도가 충분한지 먼저 확인해야 한다.
SDK가 주는 공통 편의는 비슷하다. Agent Card, Message, Task 같은 데이터 모델을 직접 문자열로 조립하지 않아도 된다. 서버에서는 요청 핸들러, 실행기, Task 저장소, 라우팅 구성을 제공한다. 클라이언트에서는 Agent Card 조회, 전송 방식 선택, 비스트리밍 또는 스트리밍 호출을 도와준다. 다만 모든 언어 SDK가 같은 시점에 같은 수준의 기능을 제공한다고 가정해서는 안 된다. 실제 프로젝트에서는 선택한 언어의 SDK 문서와 샘플을 기준으로 구현 범위를 확정해야 한다.
.NET 환경에서는 원격 A2A 엔드포인트를 일반 에이전트처럼 감싸는 방식도 제공된다. 예를 들어 Agent Card를 해석해 AIAgent 형태로 만들고, 표준 요청은 RunAsync, 스트리밍 요청은 RunStreamingAsync로 호출하는 식이다.2 이 패턴은 A2A를 별도 네트워크 프로토콜로 직접 다루기보다, 애플리케이션 내부의 에이전트 추상화에 연결하고 싶을 때 유용하다.
최소 A2A 서버의 네 가지 구성요소
가장 작은 A2A 서버는 네 가지를 갖춘다.
첫째, Agent Card다. Agent Card는 이 에이전트의 공개 소개서다. 이름, 설명, 버전, 엔드포인트, 입력과 출력 형식, 제공하는 스킬, 인증 요구사항을 담는다. 클라이언트는 이 정보를 보고 이 에이전트가 현재 요청을 처리할 수 있는지 판단한다.
둘째, 실제 에이전트 로직이다. 예제에서는 Hello World를 돌려주는 함수 하나일 수 있다. 실무에서는 견적 계산, 문서 분석, 고객 이력 조회, 승인 초안 작성 같은 전문 로직이 들어간다. 중요한 점은 이 내부 로직이 A2A 명세와 직접 섞일 필요가 없다는 것이다. 내부 로직은 도메인 일을 하고, A2A 계층은 외부와 주고받는 형식을 책임진다.
셋째, Agent Executor다. Executor는 A2A 요청을 받아 내부 에이전트 로직으로 넘기는 어댑터다. Python SDK의 공식 튜토리얼 흐름에서는 AgentExecutor 구현체를 만들고, execute와 cancel 같은 메서드를 정의한다. 서버 구성에서는 이 실행기를 DefaultRequestHandler에 연결하고, InMemoryTaskStore 같은 Task 저장소를 붙인 뒤, Agent Card 라우트와 JSON-RPC 라우트를 Starlette 애플리케이션에 등록해 Uvicorn으로 실행한다.3
넷째, Task 저장소다. 처음에는 메모리 저장소로 충분하다. 하지만 운영 환경에서는 프로세스 재시작, 장애 복구, 감사 로그, 재조회 요구를 고려해야 한다. 장기 실행 업무를 다룬다면 Task 상태와 Artifact를 영속 저장소에 남기는 구조가 필요하다.
구조를 의사 코드로 표현하면 다음과 같다.
Agent Card 정의
- 이름, 설명, URL, 버전
- 입력 모드와 출력 모드
- 스킬 목록
- 인증 요구사항
내부 에이전트 구현
- invoke 요청을 받아 실제 업무 수행
- 결과 텍스트 또는 구조화 데이터 반환
Agent Executor 구현
- execute에서 요청 메시지 해석
- 내부 에이전트 호출
- 결과를 Message 또는 Artifact 이벤트로 큐에 추가
- cancel에서 취소 가능 여부 처리
A2A 서버 시작
- Request Handler 생성
- Task Store 연결
- Agent Card 경로 공개
- A2A 호출 라우트 등록
- HTTP 서버 실행이 구조에서 서버는 단순히 HTTP 요청에 문자열을 반환하는 API가 아니다. 들어온 메시지를 A2A의 실행 맥락으로 해석하고, Task 상태와 산출물을 외부에 전달할 수 있는 에이전트 인터페이스가 된다.
Agent Card는 첫 번째 테스트 대상이다
첫 구현에서 가장 먼저 확인할 것은 모델 응답이 아니라 Agent Card다. 클라이언트가 /.well-known/agent-card.json 같은 표준 위치에서 카드를 가져올 수 있어야 한다. 카드의 URL은 실제 서버 주소와 맞아야 하고, 스킬 설명은 과장하지 않아야 하며, 지원하지 않는 입력 형식을 지원한다고 적으면 안 된다.
Agent Card를 느슨하게 작성하면 클라이언트가 잘못된 기대를 갖는다. 예를 들어 문서 파일 분석을 지원하지 않는데 file 입력을 표시하면, 다른 에이전트는 파일 Artifact를 보내도 된다고 판단할 수 있다. 반대로 스트리밍을 지원하면서 카드에 표시하지 않으면, 클라이언트는 비스트리밍 호출만 사용하게 된다.
따라서 Agent Card는 문서가 아니라 계약에 가깝다. 사람이 읽기 쉬운 설명도 필요하지만, 더 중요한 것은 기계가 호출 결정을 내릴 수 있을 만큼 정확한 정보다.
최소 A2A 클라이언트 흐름
클라이언트 구현은 서버보다 더 직관적이다. 순서는 대체로 다음과 같다.
1. 원격 에이전트의 기본 URL을 안다.
2. Agent Card Resolver로 Agent Card를 가져온다.
3. 카드의 엔드포인트, 기능, 인증 요구사항을 확인한다.
4. ClientConfig를 만든다.
5. 사용자 요청을 Message로 만든다.
6. SendMessageRequest로 감싼다.
7. send_message를 호출한다.
8. 반환된 Task 또는 Message를 읽는다.공식 Python 튜토리얼은 httpx.AsyncClient와 A2ACardResolver로 Agent Card를 가져오고, ClientConfig(streaming=False)로 비스트리밍 클라이언트를 만든 뒤, 사용자 텍스트를 Message로 구성해 SendMessageRequest로 전송하는 흐름을 보여준다. 비스트리밍 모드에서도 SDK의 send_message는 비동기 반복자 형태로 결과를 돌려줄 수 있으며, 예제에서는 최종 Task가 한 번 반환된다.4
응답을 받을 때는 텍스트만 보지 말고 Task 필드를 함께 확인해야 한다. task.id는 이후 조회나 추적에 필요하다. context_id는 같은 업무 맥락을 이어갈 때 중요하다. status.state가 completed인지, input-required인지, failed인지에 따라 다음 행동이 달라진다. artifacts에는 사람이 읽을 답변뿐 아니라 다른 시스템이 재사용할 구조화 데이터가 들어갈 수 있다.
비스트리밍에서 스트리밍으로 확장하기
첫 단계에서는 비스트리밍 요청·응답으로 충분하다. 클라이언트가 메시지를 보내고, 서버가 작업을 끝낸 뒤 최종 Task 또는 Message를 돌려준다. 이 방식은 짧은 요약, 간단한 분류, 규칙 기반 검증처럼 응답 시간이 짧은 작업에 적합하다.
다음 단계는 스트리밍이다. 스트리밍을 켜면 클라이언트는 최종 결과만 기다리지 않고 상태 변화를 받는다. 공식 Python 예제에서는 스트리밍 호출 시 submitted, working, Artifact 업데이트, completed 순서의 이벤트 흐름을 확인할 수 있다.4 사용자는 작업이 멈춘 것인지 진행 중인지 알 수 있고, 상위 에이전트는 중간 산출물을 먼저 활용할 수도 있다.
그다음은 푸시 알림이다. 스트리밍은 연결이 유지되는 동안 유용하다. 하지만 긴 업무에서는 클라이언트가 계속 연결되어 있지 않을 수 있다. 이때 서버가 작업 완료나 입력 필요 상태를 웹훅 같은 방식으로 알려주는 푸시 알림이 필요하다. 첫 버전에서 반드시 구현할 필요는 없지만, 배치성 업무나 사람 승인 대기가 포함된 업무라면 초기 설계에 알림 엔드포인트와 재조회 전략을 넣어 두는 편이 좋다.
구현 범위는 다음 순서로 넓히는 것이 안전하다.
텍스트 입력과 텍스트 출력만 지원하는 비스트리밍 서버
Agent Card 공개와 클라이언트 조회
Task ID, 상태, Artifact 확인
오류, 거절, 취소 처리
스트리밍 상태 업데이트
장기 작업용 영속 Task 저장소
푸시 알림과 재조회
인증, 인가, 감사 로그 강화
이 순서를 따르면 프로토콜의 핵심을 유지하면서 복잡도를 단계적으로 늘릴 수 있다.
ADK와 함께 쓰는 두 가지 패턴
Google의 Agent Development Kit, 즉 ADK를 사용하는 팀이라면 A2A를 별도 세계로 볼 필요가 없다. ADK 문서는 A2A와 함께 다중 에이전트 시스템을 구성하는 패턴을 설명하며, 루트 에이전트, 로컬 하위 에이전트, 원격 A2A 에이전트가 함께 동작하는 예를 제시한다. 또한 ADK 에이전트를 A2A로 노출하는 방식과, ADK 에이전트가 원격 A2A 에이전트를 소비하는 방식을 Python, Go, Java 중심으로 안내한다.5
첫 번째 패턴은 ADK 에이전트를 A2A 서버로 노출하는 것이다. 내부 구현은 ADK로 유지한다. 바깥에는 Agent Card와 A2A 엔드포인트를 제공한다. 다른 에이전트는 이 에이전트가 ADK로 만들어졌는지 알 필요가 없다. Agent Card를 읽고 A2A 메시지를 보내면 된다.
두 번째 패턴은 ADK 에이전트가 원격 A2A 에이전트를 하위 에이전트처럼 사용하는 것이다. 루트 에이전트가 사용자의 요청을 해석하고, 특정 업무를 원격 A2A 에이전트에 위임한다. 이때 원격 에이전트의 내부 도구와 프롬프트는 원격에 남아 있다. 루트 에이전트는 A2A Task 상태와 Artifact만 보고 다음 결정을 내린다.
이 구조는 앞 장에서 설명한 MCP와 A2A의 분리를 그대로 따른다. ADK 에이전트 내부에서는 MCP로 도구와 데이터에 접근할 수 있다. 에이전트 사이의 협업은 A2A로 처리한다. 도구 호출과 업무 위임을 분리하면 권한, 로그, 장애 처리의 경계가 선명해진다.
첫 구현에서 피해야 할 실수
첫째, Agent Card를 대충 만들지 말아야 한다. Agent Card는 README가 아니라 호출 계약이다. 지원 기능, 인증, 입출력 형식이 실제 구현과 맞아야 한다.
둘째, Task 상태를 단순 성공·실패로만 보지 말아야 한다. A2A에서는 input-required, canceled, rejected 같은 상태가 협업의 중요한 신호다. 특히 사람의 승인이 필요한 업무를 오류로 처리하면 상위 에이전트가 잘못된 재시도를 할 수 있다.
셋째, 스트리밍을 UI 효과로만 이해하지 말아야 한다. 스트리밍은 장기 작업의 상태 공유 방식이다. 중간 Artifact, 진행률, 입력 요청을 전달하는 운영 인터페이스다.
넷째, 클라이언트가 원격 에이전트의 내부 도구를 직접 안다고 가정하지 말아야 한다. A2A의 장점은 내부 구현을 숨기고도 협업할 수 있다는 점이다. 원격 에이전트가 어떤 데이터베이스나 MCP 서버를 쓰는지는 원칙적으로 원격 에이전트의 책임이다.
첫 A2A 에이전트는 작아도 된다. 중요한 것은 구조가 맞는 것이다. Agent Card로 발견되고, Message를 받아 Task로 처리하며, 상태와 Artifact를 표준 형식으로 돌려주는 순간, 그 에이전트는 더 큰 다중 에이전트 시스템 안에 들어갈 준비를 마친다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.