MCP와 A2A를 활용한 프로덕션 급 AI 에이전트 구축: 현장 경험을 바탕한 가이드

발행: (2025년 12월 25일 오후 01:40 GMT+9)
18 분 소요
원문: Dev.to

Source: Dev.to

TL;DR 이 기사에서는 취약하고 맞춤형으로 만든 AI‑에이전트 아키텍처에서 Model Context Protocol (MCP) 를 활용한 견고하고 표준화된 접근 방식으로 전환한 여정을 공유합니다. 다음을 안내합니다:

  • Agent‑to‑Agent (A2A) 통신이 프로덕션 시스템에서 빠진 연결 고리인지.
  • 표준화된 계약을 사용해 Daily Minutes Assistant 를 설계한 방법.
  • 제가 구축한 정확한 코드 인프라(그리고 여러분도 구축할 수 있는 방법).
  • 왜 나는 프롬프트 를 표준화하기보다 컨텍스트 를 표준화하는 것이 더 중요하다고 믿는지.

에이전트가 함수 호출을 환각하는 디버깅에 지쳤다면, 이 글이 도움이 될 것입니다.

혼돈에서 계약으로: 어떻게 나는 에이전시적인 서부 개척시대를 길들였는가

TL;DR

이 글에서는 취약하고 맞춤형으로 구축된 AI‑에이전트 아키텍처에서 Model Context Protocol (MCP) 을 활용한 견고하고 표준화된 접근 방식으로 전환한 과정을 공유합니다. 다음 내용을 차례대로 안내합니다:

  • Agent‑to‑Agent (A2A) 통신이 실제 운영 시스템에서 빠진 연결 고리인지를 설명합니다.
  • 표준화된 계약을 이용해 Daily Minutes Assistant 를 설계한 방법을 소개합니다.
  • 제가 직접 구축한 정확한 코드 인프라스트럭처와 여러분도 동일하게 구현할 수 있는 방법을 제시합니다.
  • 프롬프트 를 표준화하기보다 컨텍스트 를 표준화하는 것이 더 중요하다고 생각하는 이유를 논합니다.

에이전트가 함수 호출을 잘못 생성하는(환각) 문제를 디버깅하는 데 지치셨다면, 이 글이 도움이 될 것입니다.

소개

나는 첫 번째 복잡한 다중 에이전트 시스템을 디버깅하던 늦은 밤을 아직도 기억한다. 나는 Research AgentWriter Agent와 대화하도록 만들었다. Jupyter 노트북에서는 아름답게 동작했지만, 배포하는 순간… 혼돈이었다.

  • Research Agent는 JSON을 출력했지만, Writer Agent는 Markdown을 기대했다.
  • “Memory” 모듈은 전역 딕셔너리였으며 계속 덮어쓰기 되었다.

그것은 카드 한 장씩 쌓아 올린 집과 같았다.

내 경험에 비추어 볼 때, 오늘날 대부분의 AI 엔지니어링이 정체되는 지점은 바로 여기다. 우리는 인상적인 데모를 만들지만, 생산 환경에서의 신뢰성은 기본적인 프로토콜이 부족하기 때문에 얻지 못한다.

그때 나는 **MCP (Model Context Protocol)**와 같은 일반 프로토콜을 발견했다. 문제는 프롬프트 엔지니어링이 아니라 내 아키텍처라는 것을 깨달았다. 더 똑똑한 모델이 필요했던 것이 아니라, 더 나은 계약이 필요했다. 내 의견으로는, 엄격한 프로토콜을 채택하는 것이 장난감과 도구를 구분짓는 차이이다.

이 글이 다루는 내용

이 글은 “AI의 미래”와 같은 고수준의 허황된 글이 아닙니다. 이것은 제가 프로덕션‑그레이드 Agent‑to‑Agent (A2A) 시스템을 구축한 실전‑코드 중심의 상세 가이드입니다.

제가 Daily Minutes Assistant(일일 회의록 도우미)를 어떻게 만들었는지 보여드리겠습니다—다음과 같은 기능을 갖춘 시스템입니다:

  1. 내 캘린더에 연결합니다.
  2. 회의 기록을 가져옵니다.
  3. 기록을 요약합니다.
  4. 행동 항목을 이메일로 전송합니다.

“마법”은 요약이 아니라 플러밍입니다.


기술 스택

저는 과대광고보다 신뢰성을 원했기 때문에 이 스택을 선택했습니다:

구성 요소이유
Python 3.12+강력한 타입 지정, 비동기 지원
MCP SDK (mcp‑python)표준화된 도구 및 리소스 정의를 위한 핵심 백본
FastAPI / FastMCP서버 인터페이스를 빠르게 구축
Pydantic견고한 데이터 검증

왜 읽어야 할까?

만약 다음과 같은 고통을 겪어본 적이 있다면:

  • 새로운 도구마다 맞춤형 API 래퍼를 작성해야 할 때.
  • 에이전트가 언제 멈춰야 할지 몰라서 루프에 빠질 때.
  • 로컬 특화 에이전트를 클라우드 기반 LLM에 연결하려고 할 때.

…그렇다면 제가 만든 이 실험적인 PoC가 여러분의 영혼에 직접 말을 걸 것입니다. 저는 6개월 전 누군가가 이 패턴을 보여줬다면 좋았을 텐데 하는 마음에 이 글을 썼습니다.


Let’s Design

코드를 한 줄도 작성하기 전에, 나는 상호작용을 설계하기 위해 한 걸음 물러섰다. 나는 생각했다: “이 에이전트들이 직원이라면, 어떻게 문서를 전달할까?”

내가 보기에는, 에이전트가 필요로 하는 것이 세 가지이다:

  1. Tools – 에이전트가 할 수 있는 것들 (웹 검색, 이메일 전송).
  2. Resources – 에이전트가 읽을 수 있는 것들 (캘린더, 로그).
  3. Prompts – 무언가를 요청하는 표준화된 방법.

다음과 같은 흐름을 간략히 스케치했다 (단순화):

Client  →  FastMCP Server (exposes tools/resources)  →  LLM

나는 SummaryServerResearchServer를 전혀 알지 못하도록 설계했다. 이들을 분리함으로써 나중에 연구 엔진을 교체하더라도 캘린더 통합이 깨지지 않는다.


Source:

요리를 시작해봅시다

아래는 실제 구현 예시입니다. 저는 Server(기능을 제공)와 Client(그 기능을 소비) 를 분리하도록 프로젝트를 구조화했습니다.

Step 1 – FastMCP 서버

import os
from mcp.server.fastmcp import FastMCP

# FastMCP 서버 초기화.
# 제 생각에 서버 이름을 명확히 하는 것이 멀티‑에이전트 디버깅에 중요합니다.
mcp = FastMCP("DailyAssistant")

@mcp.tool()
async def search_web(query: str, limit: int = 5) -> str:
    """
    주어진 쿼리에 대해 웹을 검색합니다.

    Args:
        query: 검색 쿼리.
        limit: 반환할 최대 결과 수.
    """
    # 실제 배포에서는 Tavily, Serper 등을 호출합니다.
    # 이번 PoC에서는 프로토콜에 집중하기 위해 반환값을 모킹합니다.
    return (
        f"Mock search results for '{query}':\n"
        "1. Result A\n"
        "2. Result B"
    )

@mcp.resource("config://app_settings")
def get_app_settings() -> str:
    """애플리케이션 설정을 가져옵니다."""
    return "Theme: Dark\nNotifications: Enabled"

이 코드가 하는 일

  • search_web toolconfig://app_settings resource 를 제공하는 서버를 정의합니다.

이렇게 구조화한 이유

  • 데코레이터(@mcp.tool())를 사용하면 정의를 구현 바로 옆에 두게 됩니다.
  • 도구를 별도의 JSON 파일에 정의하면 구현이 바뀌어도 스키마가 업데이트되지 않아 불일치가 발생하기 쉽습니다.

배운 점

  • 타입 힌트(query: str)는 단순히 보기 좋은 것이 아닙니다. MCP는 이를 이용해 LLM이 최종적으로 보게 되는 JSON 스키마를 생성합니다. 여기서 타입을 대충 하면 에이전트가 나중에 혼란스러워합니다.

Step 2 – 에이전트 클라이언트

import asyncio
import sys
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def run_client():
    # 로컬 통신을 위해 Stdio를 사용하기로 했습니다.
    # 사이드‑카 패턴에서 기본적으로 더 빠르고 안전합니다.
    server_params = StdioServerParameters(
        command=sys.executable,
        args=["src/server/agent_server.py"],
        env=None,
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # 예시: 서버에 웹 검색을 요청합니다.
            result = await session.call_tool(
                "search_web",
                {"query": "MCP protocol overview", "limit": 3},
            )
            print("Search result:", result)

if __name__ == "__main__":
    asyncio.run(run_client())

이 코드가 하는 일

  • agent_server.py 를 실행하는 로컬 stdio 서브프로세스를 띄웁니다.
  • ClientSession 을 사용해 search_web 도구를 구현 방식에 신경 쓰지 않고 호출합니다.

왜 중요한가

  • 클라이언트는 계약 (도구 이름 + JSON 스키마)만 알면 됩니다.
  • 서버 구현을 교체해도(예: 원격 FastAPI 엔드포인트로 이동) StdioServerParameters 만 바꾸면 됩니다.

Step 3 – 전체 연결 (FastAPI 래퍼)

HTTP 기반 서버를 stdio 대신 사용하고 싶다면, 동일한 데코레이터를 FastAPI와 함께 사용할 수 있습니다:

from fastapi import FastAPI
from mcp.server.fastapi import FastAPIMCP

app = FastAPI()
mcp = FastAPIMCP(app, "DailyAssistant")

@mcp.tool()
async def send_email(to: str, subject: str, body: str) -> str:
    """
    모킹된 이메일 발송기.
    """
    # 실제 서비스에서는 SMTP 또는 SendGrid와 연동합니다.
    return f"Email sent to {to} with subject '{subject}'."

uvicorn this_module:app --reload 로 실행하면 이제 send_email 도구가 HTTP를 통한 MCP 프로토콜로 노출됩니다.


Lessons Learned

LessonExplanation
Contracts trump prompts잘 정의된 스키마(도구 이름, 인수, 반환 타입)는 LLM이 호출을 “환상”하는 것을 방지합니다.
Decouple producers & consumers서버가 누가 호출하는지 모르게 유지하세요. 이렇게 하면 독립적인 버전 관리를 할 수 있습니다.
Type hints are your friendsMCP는 Python 타입 힌트에서 JSON 스키마를 자동으로 생성합니다. 힌트가 없거나 잘못되면 에이전트가 손상됩니다.
Standardize context, not just output에이전트 간에 공통 컨텍스트(예: 캘린더 ID, 사용자 선호도)를 공유하면 중복과 오류를 줄일 수 있습니다.
Prefer local IPC for side‑carsstdio는 빠르고 안전하며, 두 프로세스가 동일 호스트에서 실행될 때 네트워크 지연을 피합니다.

TL;DR (Revisited)

  • MCP를 사용하여 Python 데코레이터로 toolsresources를 정의합니다.
  • agents를 얇게 유지합니다: 그들은 무엇을 호출할 수 있는지만 알면 되고, 어떻게 구현되는지는 알 필요가 없습니다.
  • 서비스를 분리 → 구현을 교체해도 계약이 깨지지 않게 합니다.

최종 생각

MCP와 같은 프로토콜을 통해 context를 표준화하면서 나의 불안정한 데모가 프로덕션‑레디 시스템으로 바뀌었습니다. 깔끔한 계약에 투자한 노력은 여러 번 보상을 받았습니다: 버그 감소, 디버깅 용이, 그리고 에이전트를 독립적으로 확장할 수 있는 능력 등.

단일‑에이전트 개념 증명을 넘어서는 무언가를 구축하고 있다면, MCP를 시도해 보세요. 미래의 여러분(그리고 운영 팀)에게 큰 도움이 될 것입니다.

세션 예시

async with MCPClientSession(read, write) as session:
    await session.initialize()

    # Dynamic Discovery
    tools = await session.list_tools()
    print(f"Connected! Found tools: {[t.name for t in tools.tools]}")

    # Execution
    result = await session.call_tool(
        "search_web",
        arguments={"query": "MCP adoption"}
    )
    print(f"Tool Output: {result.content[0].text}")

이것이 수행하는 작업

서버를 서브프로세스로 실행하고 표준 입력/출력을 통해 연결합니다. 그런 다음 동적으로 “무엇을 할 수 있나요?”(list_tools)를 물어본 뒤, 무언가를 수행하도록 요청합니다.

여기서의 경험

처음에는 모든 것을 HTTP로 사용하려고 했지만, 노트북에서 실행되는 코딩 어시스턴트 같은 로컬 에이전트의 경우 stdio가 훨씬 우수합니다. 네트워크 오버헤드가 전혀 없고 인증 흐름도 단순해집니다(프로세스를 실행할 수 있으면 접근 권한이 있는 것이기 때문입니다).


설정하기

직접 이 PoC를 실행하고 싶다면, 아주 간단하게 만들었습니다.

사전 요구사항

  • Python 3.10+
  • 가상 환경 (항상 venvs! 사용)

저장소 복제

(아래에 공개 저장소 링크가 제공됩니다.)

의존성 설치

pip install mcp httpx

설치 확인

python -c "import mcp; print(mcp.__version__)"

실행해 보기

시스템 실행은 간단합니다.

python src/client/agent_client.py

핸드셰이크가 성공했음을 나타내는 출력이 표시되고, 이어서 모의 검색 결과가 나타납니다.

주의할 점

일반적인 ConnectionRefused 또는 파이프 오류가 표시되면, 보통 서버 스크립트가 시작 시(예: import 누락) 충돌했기 때문이며, 핸드셰이크가 완료되기 전에 중단됩니다. 서버가 독립적으로 정상 실행되는지 항상 먼저 확인하세요!


Source:

마무리 생각

이 실험적인 “Daily Minutes Assistant”를 만들면서 AI의 미래는 단순히 더 큰 컨텍스트 윈도우에 관한 것이 아니라 구조화된 컨텍스트에 관한 것이라는 것을 깨달았습니다.

제 생각에 우리는 “프롬프트 엔지니어링”에서 컨텍스트 엔지니어링으로 이동하고 있습니다. MCP 접근법은 도구와 리소스를 일등 시민으로 다루게 해 주어, 화려한 데모와 신뢰할 수 있는 프로덕션 소프트웨어 사이의 격차를 메워 줍니다.

이 가이드가 제가 겪은 여러 어려움을 조금이라도 덜어주길 바랍니다. 코드는 공개되어 있으니 포크하고, 깨뜨리고, 여러분이 만든 것을 알려 주세요.

Tags: ai, python, mcp, agents


면책 조항

여기에 표현된 견해와 의견은 전적으로 저 개인의 것이며, 제가 속한 고용주나 어떠한 조직의 견해, 입장, 의견을 대변하지 않습니다. 이 내용은 저의 개인적인 경험과 실험을 바탕으로 하였으며, 불완전하거나 부정확할 수 있습니다. 오류나 오해는 의도된 것이 아니며, 어떤 진술이 오해되거나 잘못 전달될 경우 미리 사과드립니다.

Back to Blog

관련 글

더 보기 »