
AI 에이전트를 하나만 사용할 때는 구조가 비교적 단순하다.
사용자
→ Agent
→ Tool
→ 결과
Agent가 GitHub를 읽어야 하면 GitHub Tool을 연결한다.
DB를 조회해야 하면 DB Tool을 연결한다.
외부 API를 호출해야 하면 해당 Tool을 연결한다.
이런 구조에서는 MCP가 잘 맞는다.
Agent
→ MCP
→ GitHub
Agent
→ MCP
→ Database
Agent
→ MCP
→ Internal API
그런데 에이전트 시스템이 커지면 다른 문제가 생긴다.
예를 들어 한 회사에서 다음과 같은 에이전트를 운영한다고 해보자.
개발 Agent
보안 Agent
배포 Agent
CS Agent
재무 Agent
문서 Agent
이 에이전트들이 모두 같은 코드베이스나 같은 프레임워크로 만들어졌다는 보장은 없다.
개발 Agent는 TypeScript일 수 있다.
보안 Agent는 Python일 수 있다.
재무 Agent는 외부 SaaS에서 제공할 수도 있다.
배포 Agent는 다른 회사가 만든 Agent일 수도 있다.
이때 이런 요구가 생긴다.
개발 Agent가 보안 Agent에게
"이 PR을 보안 검토해줘"라고 요청한다.
보안 Agent가 결과를 돌려준다.
문제가 발견되면 개발 Agent가 수정한다.
수정 후 다시 보안 Agent에게 검토를 요청한다.
이건 Tool 호출과 조금 다르다.
상대방도 자체 판단과 상태를 가진 Agent다.
내부 구현을 알 필요도 없고, 어떤 모델을 쓰는지도 중요하지 않다.
필요한 것은 다음 정도다.
이 Agent가 무엇을 할 수 있는가?
어떻게 요청해야 하는가?
작업은 지금 어떤 상태인가?
결과물은 어디에 있는가?
작업이 오래 걸리면 어떻게 업데이트를 받을 것인가?
이 문제를 해결하기 위해 나온 표준이 A2A, Agent2Agent Protocol이다.
2026년 현재 A2A 공식 명세는 1.0.0까지 올라왔다.
핵심 목적은 간단하다.
서로 다른 회사,
서로 다른 프레임워크,
서로 다른 모델로 만들어진 Agent가
공통 프로토콜로 통신하게 한다.
A2A를 처음 보면 가장 먼저 나오는 질문이 있다.
MCP가 있는데 A2A가 왜 필요하지?
둘은 역할이 다르다.
MCP는 기본적으로 Agent가 Tool을 사용하는 방법을 표준화한다.
Agent
→ MCP
→ GitHub Tool
Agent
→ MCP
→ Database Tool
Agent
→ MCP
→ Xcode Tool
반면 A2A는 Agent와 Agent가 협업하는 방법을 표준화한다.
Developer Agent
→ A2A
→ Security Agent
Support Agent
→ A2A
→ Billing Agent
Travel Agent
→ A2A
→ Hotel Agent
둘을 같이 사용하면 구조는 이렇게 된다.
Developer Agent
│
│ A2A
▼
Security Agent
│
│ MCP
├── GitHub
├── SAST Tool
└── Dependency Scanner
Developer Agent는 Security Agent 내부를 알 필요가 없다.
Security Agent가 Claude를 쓰는지 GPT 계열 모델을 쓰는지도 몰라도 된다.
Security Agent가 어떤 Tool을 사용하는지도 몰라도 된다.
Developer Agent가 알아야 하는 것은 이것뿐이다.
Security Agent에게
PR 보안 검토를 요청할 수 있다.
A2A에서 중요한 개념이 바로 이 Opaque Agent다.
상대 Agent의 내부 구현을 공유하지 않고도 협업할 수 있다.
여기서 또 하나 헷갈리는 부분이 있다.
멀티 에이전트 시스템에서 흔히 이런 구조를 만든다.
Main Agent
├── Planner
├── Implementer
├── Reviewer
└── Tester
이것도 Agent끼리 통신하는 것처럼 보인다.
하지만 이런 내부 Subagent 구조를 모두 A2A로 만들 필요는 없다.
A2A가 필요한 대표적인 상황은 다음과 같다.
독립적으로 배포된 Agent
다른 조직에서 운영하는 Agent
다른 프레임워크로 만들어진 Agent
내부 구현을 공개하지 않는 Agent
독립적인 인증과 권한을 가진 Agent
예를 들어 하나의 Runtime 안에서 Planner와 Reviewer를 운영한다면 기존 Agent Framework의 내부 Handoff가 더 단순하다.
Runtime
Planner
→ Implementer
→ Reviewer
반면 다음 구조는 A2A에 잘 맞는다.
회사 A
Developer Agent
│
│ HTTPS / A2A
▼
회사 B
Security Review Agent
경계가 다르다.
어떤 Agent에게 작업을 맡기려면 먼저 그 Agent가 무엇을 할 수 있는지 알아야 한다.
A2A에서는 이를 Agent Card로 공개한다.
개념적으로는 다음과 같은 정보가 들어간다.
{
"name": "Security Review Agent",
"description": "Pull Request 보안 검토를 수행한다.",
"url": "https://security-agent.example.com",
"version": "1.4.0",
"capabilities": {
"streaming": true
},
"skills": [
{
"id": "review-pr",
"name": "Pull Request Security Review",
"description": "PR 변경 내용을 분석하고 보안 문제를 반환한다."
}
]
}
이 Agent Card를 보면 클라이언트 Agent는 대략 다음을 판단할 수 있다.
이 Agent가 필요한 작업을 지원하는가?
Streaming을 지원하는가?
어떤 Skill이 있는가?
어떤 인증 방식이 필요한가?
어떤 Endpoint로 연결해야 하는가?
웹 서비스의 OpenAPI 문서와 비슷한 역할도 있지만 목적은 다르다.
OpenAPI
→ API Capability 설명
Agent Card
→ Agent Capability 설명
A2A에서는 표준 위치를 통한 Agent Card 발견 방식도 정의한다.
https://agent.example.com/.well-known/agent-card.json
클라이언트는 이 주소에서 Agent Card를 가져와 지원 기능을 확인할 수 있다.
Agent Card는 Agent Discovery를 위한 공개 문서가 될 수 있다.
따라서 이런 내용을 넣으면 안 된다.
내부 API Key
DB 주소
내부 Prompt
System Prompt
Secret
Tool Credential
Private Network 주소
모델 인증 정보
Agent Card에는 Capability만 노출한다.
무엇을 할 수 있는가
내부 구현은 숨긴다.
어떻게 하고 있는가
이 구분이 중요하다.
예를 들어 다음 정도는 괜찮다.
{
"id": "dependency-security-review",
"name": "Dependency Security Review",
"description": "변경된 dependency의 알려진 위험 요소를 검토한다."
}
하지만 내부 구현까지 공개할 필요는 없다.
우리는 내부적으로 어떤 Scanner를 쓰고,
어떤 Prompt를 쓰고,
어떤 Database를 조회한다.
A2A의 목적은 Agent 구현 공유가 아니라 Agent 협업이다.
A2A에서는 모든 요청이 같은 형태로 끝나지 않는다.
간단한 질문이라면 바로 응답할 수도 있다.
Client Agent
→ "현재 지원하는 iOS 버전이 뭐야?"
Server Agent
→ "iOS 18 이상입니다."
하지만 복잡한 작업은 시간이 필요하다.
이 PR의 변경 파일 34개를 분석하고
보안 문제가 있는지 검사하고
위험도별로 정리하고
수정 방법까지 반환해줘.
이런 작업은 즉시 답을 만들기 어렵다.
그래서 A2A에는 Task 개념이 있다.
Message
→ 요청
Task
→ 실행 중인 작업
Task는 고유 ID를 가진다.
{
"id": "task-security-4821",
"status": {
"state": "working"
}
}
Client Agent는 나중에 이 Task 상태를 조회할 수 있다.
tasks/get
작업을 취소할 수도 있다.
tasks/cancel
즉, A2A는 단순 RPC 호출보다 상태가 있는 작업 처리를 중요하게 본다.
실제 구현에서는 A2A Task 상태와 내부 Runtime 상태를 연결하는 것이 좋다.
예를 들어 내부 상태가 다음과 같다고 하자.
type InternalTaskState =
| "queued"
| "planning"
| "executing"
| "reviewing"
| "completed"
| "failed";
외부에 내부 상태를 그대로 공개할 필요는 없다.
A2A에서 사용하는 상태로 매핑한다.
function mapTaskState(
state: InternalTaskState
): string {
switch (state) {
case "queued":
case "planning":
case "executing":
case "reviewing":
return "working";
case "completed":
return "completed";
case "failed":
return "failed";
}
}
중요한 원칙은 다음이다.
외부 Protocol State
≠
내부 Runtime State
내부 구현이 바뀌어도 외부 A2A 계약은 유지할 수 있어야 한다.
A2A 메시지는 단순 문자열 하나만 전달하는 구조가 아니다.
Message 안에는 여러 Part가 들어갈 수 있다.
예를 들어 PR 검토 요청이라면 다음 정보를 함께 전달할 수 있다.
Text
Structured Data
File
Reference
개념적으로는 다음과 같다.
{
"role": "user",
"parts": [
{
"kind": "text",
"text": "이 PR의 보안 위험을 검토해주세요."
},
{
"kind": "data",
"data": {
"repository": "my-company/ios-app",
"pullRequest": 482
}
}
]
}
이 방식의 장점은 자연어와 구조화된 데이터를 분리할 수 있다는 것이다.
Text
→ 사람과 Agent가 이해할 설명
Data
→ 정확한 실행 입력
예를 들어 다음처럼 자연어만 전달하는 것보다
우리 iOS 저장소의 482번 PR 좀 확인해줘.
구조화된 입력을 같이 보내는 편이 안정적이다.
{
"repository": "my-company/ios-app",
"pullRequest": 482
}
Agent 작업 결과가 단순한 답변 한 줄이 아닐 수도 있다.
보안 Agent라면 다음 파일을 만들 수 있다.
security-report.json
security-report.md
dependency-findings.json
A2A에서는 이런 결과물을 Artifact로 표현한다.
{
"artifactId": "artifact-security-report",
"name": "Security Review Report",
"parts": [
{
"kind": "data",
"data": {
"verdict": "needs_changes",
"critical": 0,
"high": 1,
"medium": 3
}
}
]
}
중요한 차이가 있다.
Message
→ 대화
Artifact
→ 작업 결과물
예를 들어 이런 문장은 Message다.
보안 검토가 완료되었습니다.
반면 실제 결과는 Artifact다.
security-review.json
이 구분을 명확하게 해두면 다음 Agent가 자연어를 다시 파싱할 필요가 없다.
이전에 Typed Agent Contract를 적용했다면 A2A와 잘 연결된다.
예를 들어 Security Review Artifact의 구조를 정의한다.
type SecurityReviewArtifact = {
schema: "security-review";
version: "1.0.0";
verdict:
| "pass"
| "needs_changes"
| "blocked";
findings: SecurityFinding[];
reviewedFiles: string[];
artifacts: string[];
humanReviewRequired: boolean;
};
type SecurityFinding = {
id: string;
severity:
| "low"
| "medium"
| "high"
| "critical";
file: string;
line?: number;
category: string;
description: string;
evidence: string;
recommendation: string;
};
A2A가 Agent 간 전송을 담당한다.
Contract는 Payload의 의미를 담당한다.
A2A
→ 어떻게 전달할 것인가
Typed Contract
→ 무엇을 전달할 것인가
이 둘을 분리해야 한다.
개념적인 전체 흐름을 보면 이해하기 쉽다.
Developer Agent가 Security Agent에게 작업을 요청한다고 하자.
1. Developer Agent
Agent Card 조회
GET
/.well-known/agent-card.json
Security Agent가 PR Review Skill을 지원하는지 확인한다.
2. Developer Agent
message/send
요청 Payload를 전달한다.
{
"message": {
"role": "user",
"parts": [
{
"kind": "text",
"text": "PR 보안 검토를 수행해주세요."
},
{
"kind": "data",
"data": {
"repository": "company/ios-app",
"pullRequest": 482
}
}
]
}
}
Security Agent는 Task를 반환한다.
{
"id": "task-482-security",
"status": {
"state": "working"
}
}
작업이 완료되면 Artifact가 생성된다.
task-482-security
└── security-review.json
Developer Agent는 결과를 읽는다.
verdict
→ needs_changes
그리고 필요한 수정만 수행한다.
Agent 작업은 API 호출처럼 항상 200ms 안에 끝나지 않는다.
특히 다음 작업은 오래 걸릴 수 있다.
대형 저장소 코드 분석
긴 문서 생성
대규모 로그 분석
테스트 실행
보안 Scan
데이터 분석
영상 처리
A2A는 Streaming을 지원한다.
예를 들어 Client가 다음과 같이 요청한다.
message/stream
Server는 SSE를 통해 상태를 계속 보낼 수 있다.
working
artifact chunk received
working
completed
Client Agent는 긴 요청이 끝날 때까지 연결을 막연하게 기다리지 않아도 된다.
진행 상황을 Runtime에 반영할 수도 있다.
type RemoteAgentProgress = {
taskId: string;
state: string;
message?: string;
artifactCount: number;
};
UI가 있다면 사용자에게 상태를 보여줄 수도 있다.
보안 검토 중
파일 분석
18 / 34
Dependency 검사 완료
최종 결과 생성 중
Streaming을 사용할 때 주의할 부분이 있다.
중간 Artifact가 전달됐다고 바로 다음 행동을 실행하면 안 된다.
예를 들어 Security Agent가 Streaming 중 이런 결과를 보냈다고 하자.
{
"severity": "high",
"file": "AuthManager.swift"
}
Developer Agent가 즉시 파일을 수정해버리면 문제가 생길 수 있다.
아직 최종 판단이 아닐 수 있기 때문이다.
따라서 Artifact 상태를 구분한다.
type ArtifactState =
| "partial"
| "final";
실행 가능한 판단은 final Artifact만 사용한다.
Partial Artifact
→ UI 표시 / 진행 상황
Final Artifact
→ 다음 Agent 실행 입력
Streaming과 실행 Trigger를 분리하는 것이 안전하다.
모든 Client가 SSE 연결을 계속 유지할 수 있는 것은 아니다.
작업이 30분 걸릴 수도 있다.
대용량 Repository 분석
장기 데이터 처리
수천 개 파일 Scan
Client 프로세스가 재시작될 수도 있다.
이런 상황에서는 완료 후 Server가 Client에게 알려주는 방식이 필요하다.
A2A는 장시간 작업을 고려해 Push Notification 형태의 비동기 업데이트도 지원한다.
구조는 다음과 같다.
Client Agent
→ 작업 요청
Remote Agent
→ Task 생성
Client 연결 종료
Remote Agent
→ 작업 계속
작업 완료
Remote Agent
→ Callback
→ Client Runtime
이 구조를 사용하면 Agent Task를 HTTP 요청 수명과 분리할 수 있다.
실무에서 A2A 호출 코드를 모든 Agent가 직접 작성하게 하면 안 된다.
Adapter를 만든다.
src/
└── a2a/
├── client/
│ ├── A2AClient.ts
│ ├── AgentDiscovery.ts
│ └── TaskClient.ts
│
├── contracts/
│ ├── AgentCard.ts
│ ├── RemoteTask.ts
│ └── Artifact.ts
│
└── policy/
├── AgentAllowlist.ts
└── ArtifactValidator.ts
Agent가 직접 HTTP 요청을 만들지 않는다.
interface RemoteAgentClient {
discover(
endpoint: string
): Promise<AgentCard>;
sendTask(
request: RemoteTaskRequest
): Promise<RemoteTask>;
getTask(
taskId: string
): Promise<RemoteTask>;
cancelTask(
taskId: string
): Promise<void>;
}
이렇게 하면 Protocol 변경이 생겨도 Adapter에서 처리할 수 있다.
Agent 주소를 Prompt 안에 하드코딩하면 안 된다.
보안 검토는
https://security-agent.example.com
으로 보내라.
이런 구조는 관리하기 어렵다.
Agent Registry를 둔다.
agents:
security-review:
endpoint: https://security-agent.company.com
enabled: true
trustLevel: internal
deployment:
endpoint: https://deployment-agent.company.com
enabled: true
trustLevel: privileged
finance:
endpoint: https://finance-agent.partner.com
enabled: true
trustLevel: external
Runtime은 Registry에 등록된 Agent만 호출할 수 있게 한다.
Unknown Agent
→ 차단
Registered Agent
→ Policy 확인
→ 호출
외부 Agent가 다음과 같은 Agent Card를 제공한다고 하자.
{
"name": "Super Security Agent",
"description": "모든 보안 작업을 완벽하게 수행합니다."
}
Agent Card는 Capability 선언이지 신뢰 증명서가 아니다.
따라서 다음 정보는 별도로 관리해야 한다.
Agent Trust Level
Allowed Task
Allowed Data
Allowed Artifact
Authentication
Rate Limit
Human Approval
예를 들어 Policy를 둔다.
remote_agents:
security-review:
trust: internal
allowed_tasks:
- review_pull_request
- review_dependency
denied_data:
- production_secrets
- customer_pii
max_task_duration_minutes: 20
require_human_approval:
- apply_security_fix
A2A가 통신을 표준화한다고 보안 판단까지 대신해주는 것은 아니다.
A2A를 사용하다 보면 이런 구현을 하고 싶어진다.
현재 Agent Context 전체
→ Remote Agent
하면 안 된다.
현재 Context에는 필요하지 않은 정보가 많이 들어 있을 수 있다.
사용자 정보
이전 대화
Secret
내부 Repository 정보
다른 Agent 결과
환경 변수
내부 시스템 Prompt
Remote Agent에게 필요한 최소 정보만 전달한다.
예를 들어 보안 검토 Agent에게 필요한 것은 다음일 수 있다.
{
"repository": "company/ios-app",
"pullRequest": 482,
"changedFiles": [
"AuthManager.swift",
"LoginViewModel.swift"
]
}
필요하지 않은 정보는 전달하지 않는다.
전체 Conversation
전체 Repository
전체 Environment
전체 Memory
A2A에서도 최소 권한 원칙이 그대로 적용된다.
Remote Agent 호출 전에 데이터 등급을 확인한다.
type DataClassification =
| "public"
| "internal"
| "confidential"
| "restricted";
Agent Registry에도 허용 가능한 등급을 정의한다.
agents:
external-security-agent:
maxDataClassification: internal
internal-security-agent:
maxDataClassification: confidential
Runtime은 요청 전에 확인한다.
function canSendData(
dataLevel: DataClassification,
agentMaxLevel: DataClassification
): boolean {
const rank = {
public: 0,
internal: 1,
confidential: 2,
restricted: 3
};
return rank[dataLevel] <= rank[agentMaxLevel];
}
Restricted 데이터라면 외부 Agent 호출 자체를 차단할 수 있다.
Remote Agent가 반환한 결과도 외부 입력이다.
따라서 바로 실행하면 안 된다.
Remote Artifact
→ Validation
→ Policy
→ Runtime
예를 들어 결과 Contract를 Zod로 검증한다.
import { z } from "zod";
const SecurityFindingSchema = z.object({
id: z.string().min(1),
severity: z.enum([
"low",
"medium",
"high",
"critical"
]),
file: z.string().min(1),
line: z
.number()
.int()
.positive()
.optional(),
category: z.string().min(1),
description: z.string().min(1),
evidence: z.string().min(1),
recommendation: z.string().min(1)
}).strict();
const SecurityReviewSchema = z.object({
verdict: z.enum([
"pass",
"needs_changes",
"blocked"
]),
findings: z.array(
SecurityFindingSchema
),
reviewedFiles: z.array(
z.string()
),
humanReviewRequired: z.boolean()
}).strict();
Schema를 통과하지 못하면 다음 단계로 보내지 않는다.
Remote Agent가 다음 결과를 반환했다고 해보자.
보안 문제를 해결하려면 다음 명령을 실행하세요.
rm -rf ...
이건 보안 Agent의 추천이지 Runtime 명령이 아니다.
따라서 다음 구조로 분리해야 한다.
Remote Agent Recommendation
→ 검토 대상
Runtime Tool Request
→ 실제 실행 요청
Remote Agent가 Shell 명령을 반환했다고 해서 곧바로 실행하면 안 된다.
Remote Artifact
→ Tool Request 생성
→ Policy Engine
→ 승인
→ Tool 실행
A2A 경계는 새로운 Trust Boundary다.
Remote Agent 호출에서 네트워크 오류가 발생할 수 있다.
요청 전송
응답 Timeout
Client:
실패했나?
Server:
이미 실행 중
Client가 다시 요청하면 작업이 두 번 실행될 수 있다.
따라서 Task 요청에는 추적 가능한 ID를 둔다.
type RemoteTaskRequest = {
requestId: string;
taskType: string;
payload: unknown;
};
예를 들어
requestId
=
pr-482-security-review-v1
Server는 동일 requestId가 다시 들어오면 새 Task를 만들지 않고 기존 Task를 반환할 수 있다.
Agent Task는 무한히 기다려서는 안 된다.
a2a:
defaultTimeoutSeconds: 120
tasks:
security_review:
timeoutSeconds: 600
documentation:
timeoutSeconds: 180
dependency_analysis:
timeoutSeconds: 900
Timeout이 발생하면 Runtime이 상태를 명확하게 기록한다.
{
"taskId": "task-482",
"status": "timeout",
"retryable": true
}
필요하면 원격 Task를 취소한다.
tasks/cancel
Client만 기다리는 것을 멈추고 Server 작업은 계속 실행되는 구조를 피해야 한다.
A2A 요청 실패라고 모두 재시도해서는 안 된다.
예를 들어
Network Timeout
→ Retry 가능
503
→ Retry 가능
하지만
401
→ Retry 금지
Task Rejected
→ Retry 금지
Policy Violation
→ Retry 금지
정책을 만든다.
function shouldRetry(
errorCode: string
): boolean {
return [
"NETWORK_TIMEOUT",
"REMOTE_UNAVAILABLE",
"RATE_LIMITED"
].includes(errorCode);
}
Retry 횟수도 제한한다.
1차 실패
→ 1초
2차 실패
→ 3초
3차 실패
→ 중단
무한 Agent 호출 루프는 금지해야 한다.
A2A를 사용하면 이런 상황도 가능하다.
Agent A
→ Agent B
Agent B
→ Agent C
Agent C
→ Agent A
잘못 설계하면 Agent 위임 루프가 생긴다.
그래서 Handoff Depth를 제한한다.
type DelegationContext = {
rootTaskId: string;
depth: number;
visitedAgents: string[];
};
다음 호출 전에 확인한다.
function validateDelegation(
context: DelegationContext,
targetAgent: string
): void {
if (context.depth >= 4) {
throw new Error(
"Maximum delegation depth exceeded."
);
}
if (
context.visitedAgents.includes(
targetAgent
)
) {
throw new Error(
`Delegation cycle detected: ${targetAgent}`
);
}
}
Remote Agent 협업에서도 그래프 순환을 고려해야 한다.
Agent가 Agent를 부르면 비용 구조가 복잡해진다.
Developer Agent
→ Security Agent
→ Scanner Tool
→ Model
여기에 Reviewer Agent가 또 붙을 수 있다.
따라서 Task 단위 사용량을 기록한다.
{
"rootTaskId": "pr-482",
"remoteTasks": [
{
"agent": "security-review",
"taskId": "security-932",
"durationMs": 48123,
"status": "completed"
}
]
}
가능하다면 비용 정보도 함께 관리한다.
Task Duration
Model Usage
Tool Calls
Remote Agent Calls
Retry Count
Agent 호출 비용이 보이지 않으면 멀티 Agent 구조가 커질수록 운영 비용을 추적하기 어렵다.
분산 시스템에서 Trace가 중요한 것처럼 Agent 네트워크에서도 중요하다.
예를 들어 하나의 사용자 요청이 다음과 같이 흘렀다고 하자.
User Request
Developer Agent
Security Agent
Dependency Agent
Developer Agent
Tester
Final Report
모든 Task에 공통 Trace ID를 붙인다.
{
"traceId": "trace-pr-482",
"taskId": "security-task-123",
"parentTaskId": "developer-task-88",
"agent": "security-review"
}
로그를 보면 전체 흐름을 복원할 수 있다.
trace-pr-482
09:00:01
Developer Agent started
09:00:07
Security Agent delegated
09:00:09
Dependency Agent delegated
09:00:32
Dependency analysis completed
09:00:51
Security review completed
09:01:02
Developer Agent resumed
A2A를 도입하면 단일 Agent 로그만 봐서는 문제가 어디에서 생겼는지 알기 어렵다.
Distributed Trace가 필요해진다.
매번 Remote Agent 호출 전에 Agent Card를 다시 가져올 필요는 없다.
Cache할 수 있다.
type CachedAgentCard = {
card: AgentCard;
fetchedAt: number;
expiresAt: number;
};
하지만 영구 Cache는 위험하다.
Agent Capability가 바뀔 수 있기 때문이다.
Skill 추가
Skill 제거
Endpoint 변경
Authentication 변경
Streaming 지원 변경
TTL을 둔다.
agentCardCache:
ttlSeconds: 300
refreshOn:
- authentication_error
- unsupported_skill
- protocol_error
Remote Agent의 Skill이 바뀌면 Client가 깨질 수 있다.
예를 들어 기존 Skill이
security-review-v1
이었다가
security-review-v2
로 바뀌었다고 하자.
출력 Contract도 달라질 수 있다.
따라서 Agent Card에 Skill 버전이나 Contract 버전을 포함하는 전략이 필요하다.
{
"id": "review-pr",
"name": "PR Security Review",
"metadata": {
"contract": "security-review",
"contractVersion": "2.0.0"
}
}
Client는 자신이 지원하는 버전인지 확인한다.
실제 Remote Agent를 모든 테스트에서 호출하면 느리고 불안정하다.
Mock A2A Server를 만든다.
tests/
└── a2a/
├── mock-agent/
│ ├── agent-card.json
│ ├── server.ts
│ └── fixtures/
│
├── task-completed.json
├── task-failed.json
├── invalid-artifact.json
└── streaming-timeout.json
테스트해야 할 시나리오는 많다.
정상 완료
Task 실패
Timeout
Cancellation
잘못된 Artifact
Agent Card 변경
인증 실패
Streaming 중 연결 종료
중복 requestId
Agent 위임 Loop
A2A도 결국 외부 Protocol Integration이다.
Happy Path만 테스트하면 안 된다.
실제 프로젝트에서는 다음처럼 구성할 수 있다.
.ai/
├── agents/
│ ├── registry.yaml
│ └── policy.yaml
│
├── contracts/
│ ├── security-review.schema.json
│ └── deployment-result.schema.json
│
└── fixtures/
└── a2a/
src/
├── agent/
│ └── DeveloperAgent.ts
│
├── a2a/
│ ├── client/
│ │ ├── A2AClient.ts
│ │ ├── AgentDiscovery.ts
│ │ └── TaskClient.ts
│ │
│ ├── policy/
│ │ ├── AgentPolicy.ts
│ │ ├── DataPolicy.ts
│ │ └── ArtifactPolicy.ts
│ │
│ ├── registry/
│ │ └── AgentRegistry.ts
│ │
│ └── trace/
│ └── A2ATrace.ts
│
└── runtime/
└── AgentRuntime.ts
Agent 자체에는 A2A 세부 구현을 넣지 않는다.
Developer Agent
→ Runtime
Runtime
→ Agent Registry
Runtime
→ Policy
Runtime
→ A2A Client
이렇게 해야 Agent Prompt와 Protocol 구현이 분리된다.
처음부터 회사 전체 Agent를 연결하지 않는 것이 좋다.
작은 Use Case 하나부터 시작한다.
예를 들어
Developer Agent
→ Security Review Agent
정도다.
Security Agent의 Agent Card를 만든다.
한 가지 Skill만 공개한다.
review_pull_request
입력 Contract를 고정한다.
{
"repository": "company/ios-app",
"pullRequest": 482
}
출력 Artifact Contract를 고정한다.
SecurityReviewArtifact
Timeout과 Cancel을 구현한다.
Agent Registry와 Allowlist를 붙인다.
Trace ID를 기록한다.
Streaming은 필요해질 때 추가한다.
이 순서가 현실적이다.
A2A가 표준이라고 해서 모든 Agent 통신에 적용할 필요는 없다.
다음 구조라면 오히려 과할 수 있다.
하나의 Runtime
하나의 Repository
Planner
Implementer
Reviewer
Tester
이런 경우에는 내부 Typed Contract와 Framework Handoff로 충분할 수 있다.
A2A가 특히 의미 있는 지점은 경계가 생길 때다.
다른 서비스
다른 Runtime
다른 조직
다른 Vendor
다른 인증 영역
다른 Agent Framework
즉 기준은 Agent 개수가 아니다.
시스템 경계가 있는가다.
지금까지 AI Agent 개발에서는 모델과 Tool 연결이 중요한 문제였다.
Model
Prompt
Tool
Memory
MCP
Agent 시스템이 커지면서 다음 문제가 등장하고 있다.
Agent를 어떻게 발견할 것인가
Agent끼리 어떤 형식으로 요청할 것인가
장시간 작업 상태를 어떻게 관리할 것인가
결과 Artifact를 어떻게 전달할 것인가
다른 조직의 Agent를 어떻게 신뢰할 것인가
이건 단순 Prompt Engineering 문제가 아니다.
전통적인 분산 시스템에서 이미 보던 문제와 비슷하다.
Service Discovery
Protocol
Authentication
Authorization
Task Queue
Retry
Timeout
Idempotency
Distributed Trace
Schema Validation
Versioning
Agent가 많아질수록 AI 개발은 기존 백엔드·분산 시스템 설계와 가까워진다.
MCP가 등장하면서 Agent가 외부 Tool과 연결되는 방식이 빠르게 표준화됐다.
A2A는 그다음 문제를 다룬다.
MCP
Agent
→ Tool
A2A
Agent
→ Agent
실제 시스템에서는 둘을 함께 사용할 가능성이 높다.
Developer Agent
│
│ A2A
▼
Security Agent
│
│ MCP
├── GitHub
├── Security Scanner
└── Dependency Database
이 구조에서 중요한 것은 Agent끼리 말을 많이 하게 만드는 것이 아니다.
Agent Discovery
Task Lifecycle
Typed Artifact
Authentication
Policy
Timeout
Cancellation
Trace
Versioning
이런 시스템적인 경계를 만드는 것이다.
한 줄로 정리하면 이렇다.
MCP가 Agent에게 도구를 연결한다면,
A2A는 독립적인 Agent들을 하나의 시스템으로 연결한다.
앞으로 Agent 개발이 커질수록 하나의 거대한 Agent를 만드는 방식보다, 전문 Agent를 서비스처럼 분리하고 표준 프로토콜로 연결하는 구조가 늘어날 가능성이 높다.
그때 개발자가 설계해야 하는 것은 Prompt만이 아니다.
Agent 사이의 네트워크, 계약, 상태, 신뢰 경계까지 포함한 분산 시스템이다.