MCP 다음은 A2A다: 서로 다른 AI 에이전트를 연결하는 Agent2Agent 실전 설계

이경규·2026년 7월 23일

MCP 다음은 A2A다: 서로 다른 AI 에이전트를 연결하는 Agent2Agent 실전 설계

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가

공통 프로토콜로 통신하게 한다.

1. MCP와 A2A는 경쟁 관계가 아니다

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의 내부 구현을 공유하지 않고도 협업할 수 있다.


2. 내부 Subagent와 A2A 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

경계가 다르다.


3. A2A에서 가장 먼저 만나는 것이 Agent Card다

어떤 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를 가져와 지원 기능을 확인할 수 있다.


4. 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 협업이다.


5. Message와 Task를 구분해야 한다

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 호출보다 상태가 있는 작업 처리를 중요하게 본다.


6. Task State를 Runtime 상태와 연결한다

실제 구현에서는 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 계약은 유지할 수 있어야 한다.


7. Message 안에는 Part가 들어간다

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
}

8. 결과는 Artifact로 반환한다

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가 자연어를 다시 파싱할 필요가 없다.


9. Artifact를 Typed Contract로 만든다

이전에 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
→ 무엇을 전달할 것인가

이 둘을 분리해야 한다.


10. 실제 A2A 요청 흐름

개념적인 전체 흐름을 보면 이해하기 쉽다.

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

그리고 필요한 수정만 수행한다.


11. 장시간 작업에는 Streaming이 중요하다

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 검사 완료

최종 결과 생성 중

12. Streaming 결과를 바로 실행하면 안 된다

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를 분리하는 것이 안전하다.


13. 긴 작업에는 Push Notification도 필요하다

모든 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 요청 수명과 분리할 수 있다.


14. A2A Client를 Runtime 내부 Adapter로 감싼다

실무에서 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에서 처리할 수 있다.


15. Agent Registry를 따로 둔다

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 확인
→ 호출

16. Agent Card를 그대로 신뢰하면 안 된다

외부 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가 통신을 표준화한다고 보안 판단까지 대신해주는 것은 아니다.


17. 외부 Agent에 Context 전체를 넘기면 안 된다

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에서도 최소 권한 원칙이 그대로 적용된다.


18. Data Classification을 붙인다

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 호출 자체를 차단할 수 있다.


19. Artifact도 반드시 검증한다

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를 통과하지 못하면 다음 단계로 보내지 않는다.


20. Artifact 안의 지시문도 실행 명령으로 보면 안 된다

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다.


21. Idempotency를 고려한다

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를 반환할 수 있다.


22. Timeout과 Cancellation을 반드시 설계한다

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 작업은 계속 실행되는 구조를 피해야 한다.


23. Retry는 모든 오류에 적용하면 안 된다

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 호출 루프는 금지해야 한다.


24. 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 협업에서도 그래프 순환을 고려해야 한다.


25. 비용도 Task 단위로 기록한다

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 구조가 커질수록 운영 비용을 추적하기 어렵다.


26. A2A Trace를 남긴다

분산 시스템에서 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가 필요해진다.


27. Agent Card도 Cache 전략이 필요하다

매번 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

28. Agent Capability도 Versioning한다

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는 자신이 지원하는 버전인지 확인한다.


29. A2A 테스트는 Mock Agent로 한다

실제 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만 테스트하면 안 된다.


30. 가장 현실적인 프로젝트 구조

실제 프로젝트에서는 다음처럼 구성할 수 있다.

.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 구현이 분리된다.


31. A2A를 처음 도입한다면 이렇게 시작한다

처음부터 회사 전체 Agent를 연결하지 않는 것이 좋다.

작은 Use Case 하나부터 시작한다.

예를 들어

Developer Agent
→ Security Review Agent

정도다.

1단계

Security Agent의 Agent Card를 만든다.

2단계

한 가지 Skill만 공개한다.

review_pull_request

3단계

입력 Contract를 고정한다.

{
  "repository": "company/ios-app",
  "pullRequest": 482
}

4단계

출력 Artifact Contract를 고정한다.

SecurityReviewArtifact

5단계

Timeout과 Cancel을 구현한다.

6단계

Agent Registry와 Allowlist를 붙인다.

7단계

Trace ID를 기록한다.

8단계

Streaming은 필요해질 때 추가한다.

이 순서가 현실적이다.


32. A2A를 사용하지 않아도 되는 경우

A2A가 표준이라고 해서 모든 Agent 통신에 적용할 필요는 없다.

다음 구조라면 오히려 과할 수 있다.

하나의 Runtime

하나의 Repository

Planner
Implementer
Reviewer
Tester

이런 경우에는 내부 Typed Contract와 Framework Handoff로 충분할 수 있다.

A2A가 특히 의미 있는 지점은 경계가 생길 때다.

다른 서비스

다른 Runtime

다른 조직

다른 Vendor

다른 인증 영역

다른 Agent Framework

즉 기준은 Agent 개수가 아니다.

시스템 경계가 있는가다.


33. 개발자가 왜 A2A를 알아야 할까

지금까지 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 개발은 기존 백엔드·분산 시스템 설계와 가까워진다.


34. 마무리

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 사이의 네트워크, 계약, 상태, 신뢰 경계까지 포함한 분산 시스템이다.

profile
iOS 앱 개발자

0개의 댓글