Java에서의 Model Context Protocol
요약
Model Context Protocol(MCP)을 사용하여 AI 에이전트와 도구 서버 간의 연결을 표준화하는 방법을 다룹니다. Java SDK를 활용해 MCP 서버와 클라이언트를 구축하는 과정을 설명하며, M×N 통합 문제를 M+N으로 단순화하는 이점을 제시합니다.
핵심 포인트
- MCP는 AI 애플리케이션과 도구 서버 간의 와이어 포맷을 표준화함
- 표준 프로토콜을 통해 도구 서버를 한 번만 작성하면 다양한 호스트에서 재사용 가능
- JSON-RPC 2.0 기반의 도구 발견 및 실행 메커니즘 활용
- Java SDK를 이용한 MCP 서버 및 클라이언트 구현 가이드 제공
서론
모든 에이전트(Agent)에는 도구(Tool)가 필요하며, 모든 도구에는 모델에 도달할 수 있는 방법이 필요합니다. Java로 에이전트 워크플로우 구축하기 (Building Agentic Workflows in Java)에서는 수동으로 작성된 Tool 스키마와 toolUse.name()에 따라 디스패치(dispatch)하는 루프를 통해 그 연결을 직접 구현했습니다. Java에서 LLM 프레임워크 vs Raw SDK (LLM Frameworks vs. the Raw SDK in Java)에서는 LangChain4j와 Spring AI가 리플렉션(reflection)을 통해 어노테이션이 붙은 Java 메서드를 동일한 스키마로 변환하는 모습을 보여주었습니다. 두 방식 모두 여전히 _맞춤형(bespoke)_입니다. 즉, 도구가 하나의 프로세스 내에 존재하며, 하나의 언어로 작성된 하나의 에이전트에 연결되어 있습니다.
Model Context Protocol (MCP)는 다른 문제를 해결합니다. MCP는 AI 애플리케이션과 도구 서버 사이의 _와이어 포맷(wire format)_을 표준화하여, 서버를 에이전트별, 프레임워크별, 또는 언어별로 매번 다시 작성할 필요가 없도록 합니다. 이 포스트에서는 MCP를 통해 얻을 수 있는 이점이 무엇인지 다루고, 공식 Java SDK를 사용하여 최소 기능의 MCP 서버와 이를 소비하는 클라이언트를 구축하며, 직접적인 도구 호출(tool call) 대신 프로토콜을 사용하는 것이 언제 가치가 있는지에 대해 솔직한 답변을 제공합니다.
MCP가 해결하는 문제
공유된 프로토콜이 없다면, _에이전트 프레임워크(agent framework)_와 _도구(tool)_의 모든 조합마다 고유한 글루 코드(glue code)가 필요합니다. LangChain4j 도구 래퍼(wrapper), Spring AI의 @Tool 메서드, Raw SDK를 위한 수동 스키마 등, 하나의 기능을 위해 세 가지 통합 작업이 필요하며, 추가하는 모든 도구와 프레임워크에 대해 이 작업이 반복됩니다. 이는 M×N 통합 문제입니다.
MCP는 이를 M+N으로 단순화합니다. **서버(server)**는 표준 JSON-RPC 프로토콜을 통해 도구, 리소스(resources), 프롬프트(prompts)를 한 번만 노출합니다. Claude Code, Claude Desktop, VS Code 또는 사용자 자신의 에이전트와 같은 어떤 호스트(host) 애플리케이션이라도, 호스트를 구축한 프레임워크에 관계없이 동일한 프로토콜을 사용하는 MCP **클라이언트(client)**를 생성합니다. 서버를 한 번만 작성하면, MCP를 인식하는 모든 호스트가 새로운 통합 코드 없이 이를 사용할 수 있습니다.
프로토콜 자체는 의도적으로 지루하게 설계되었습니다. 수명 주기 협상 (lifecycle negotiation), 도구 발견 (tool discovery), 그리고 도구 실행 (tool execution)을 위해 JSON-RPC 2.0 메시지를 사용합니다. 이 포스트에서 중요한 두 가지 호출은 발견 (tools/list)과 실행 (tools/call)입니다.
// tools/list 응답 (축약형)
{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [
{ "name": "get_account_balance", "description": "ID를 통해 계좌 잔액 조회"},
...
두 가지 전송 방식 (transports)이 대부분의 사례를 커버합니다. stdio (로컬 서브프로세스, 서버당 하나의 클라이언트 — 이 포스트에서 사용하는 방식)와 Streamable HTTP (원격 서버, 다수의 클라이언트, 표준 bearer-token 인증)가 있습니다. 출처: MCP architecture overview.
최소 기능의 MCP 서버 구축하기
공식 Java SDK (io.modelcontextprotocol.sdk, Tier 2)는 외부 프레임워크가 필요하지 않습니다. 핵심 mcp 모듈에 stdio 및 HTTP 전송 방식이 직접 포함되어 있습니다 (Java MCP Server docs).
<dependencyManagement>
<dependencies>
<dependency>
...
도구 (tool)는 읽기 전용 계좌 잔액 조회 기능입니다. 의도적으로 단순하게 설계되었으며, 다음 섹션에서 구체적인 이유를 설명하겠지만 자유 형식의 텍스트 쿼리 대신 ID를 키로 사용하도록 설계되었습니다.
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
...
읽기 전용 리소스 (resource) — MCP의 두 번째 기본 요소(primitive)로, 모델이 특정 동작(action)을 수반하지 않고 컨텍스트에 로드할 수 있는 데이터를 의미합니다 — 를 구현하려면 몇 줄의 코드가 더 필요합니다.
server.addResource(new McpServerFeatures.SyncResourceSpecification(
Resource.builder("docs://refund-policy", "Refund Policy")
.description("현재 환불 정책 텍스트").mimeType("text/plain").build(),
...
도구/리소스 명세(specification shapes)에 대한 소스: Java MCP Server docs. StdioServerTransportProvider는 stdin/stdout을 통해 통신합니다. stdio 서버에서는 절대 System.out.println을 사용하지 마세요. 이는 JSON-RPC 스트림을 손상시킵니다 (모든 언어의 퀵스타트에서 반복되는 것과 동일한 로깅 경고입니다).
신뢰 경계(Trust Boundary)는 이동하지 않습니다
도구의 호출 핸들러(call handler) 내 request.arguments()는 원시 SDK 루프(post 14)의 toolUse.input()이나 @Tool 어노테이션이 붙은 메서드가 받는 인자(post 26)와 동일한, 신뢰할 수 없는 모델 제공 데이터입니다. MCP는 전송 형식(wire format)을 표준화할 뿐, 신뢰 모델(trust model)을 표준화하는 것이 아닙니다. SDK는 핸들러가 실행되기 전에 inputSchema를 기준으로 인자의 _형태(shape)_를 검증합니다 (마이그레이션 노트에 따라 기본적으로 활성화된 JSON Schema 2020-12 검사). 하지만 형태 검증은 accountId가 문자열임을 확인할 뿐, 그것이 안전한 문자열임을 보장하지는 않습니다. 위에서 언급한 정규 표현식(regex) 검사는 여전히 핸들러 내부에서 수행되어야 합니다.
스키마만 믿고 두 번째 검사를 생략하지 마세요:
// 위험함 — 절대 이렇게 하지 마세요: 도구 인자를 쿼리에 문자열 보간(string-interpolating)하는 경우
String sql = "SELECT balance FROM accounts WHERE id = '" + accountId + "'"; // SQL 인젝션 (SQL injection)
...
post 14의 executeValidatedTool에서 얻은 교훈은 변함없이 적용됩니다: 예상하는 형태를 화이트리스트(whitelist)로 지정하고, 그 외의 것은 거부하며, 도구 인자가 연결(concatenation)을 통해 셸 명령(shell command), 파일 경로(file path), 또는 SQL 문자열에 도달하게 하지 마세요.
서버를 소비하는 클라이언트 구축하기
클라이언트는 동일한 stdio 전송(transport)을 통해 연결하고, 도구를 발견하며, 이를 호출합니다. 서버가 어떻게 구현되었는지에 대한 지식은 필요하지 않습니다:
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.ServerParameters;
...
McpClient/ServerParameters/StdioClientTransport의 소스: Java MCP Client docs. 이는 modelcontextprotocol.io의 Build an MCP Server 튜토리얼에서 일반 Java 클라이언트로 Spring AI 날씨 서버를 테스트할 때 사용하는 것과 동일한 형태입니다. 즉, 클라이언트는 전송(transport)의 반대편에서 어떤 SDK나 프레임워크가 서버를 구축했는지 상관하지 않습니다.
14번 포스트로부터의 에이전트 루프(Agent Loop)에 MCP 연결하기
에이전트 루프 자체는 변하지 않으며, 도구(tool) 목록과 도구 실행이 어디에서 오는지만 달라집니다. 직접 작성한 Tool 객체와 로컬 executeValidatedTool 메서드 대신, MCP 클라이언트로부터 도구 목록을 나열하고 tool_use 블록을 client.callTool(...)을 통해 라우팅합니다.
import com.anthropic.core.JsonValue;
// MCP 도구의 Map 형태인 inputSchema를 Anthropic SDK의 Tool.InputSchema로 변환
...
14번 포스트의 나머지 모든 사항 — MAX_ITERATIONS 제한, ThinkingConfigAdaptive가 적용된 claude-opus-4-8, 메시지 기록 관리(bookkeeping) — 은 변경되지 않았습니다. 오직 도구의 _구현(implementation)_만이 별도의 재사용 가능한 프로세스로 이동했을 뿐입니다.
MCP vs 프레임워크 도구 추상화 (26번 포스트)
LangChain4j의 @Tool/AiServices와 Spring AI의 @Tool/ChatClient (26번 포스트)는 사용자의 자체 JVM 프로세스 내에 존재하는 메서드를 래핑(wrap)합니다. 이는 편리하지만, 도구가 해당 애플리케이션 내부에서만 존재한다는 한계가 있습니다. 반면 MCP는 별도의 프로세스 또는 서비스를 표준 프로토콜 뒤에 래핑합니다. 따라서 위에서 언급한 동일한 계정(accounts) 서버를 Claude Desktop이 실행할 수 있고, Claude Code에서 호출할 수 있으며, 이 포스트의 일반 Java 클라이언트에서 사용할 수 있습니다. 즉, 세 개의 서로 관련 없는 호스트가 존재함에도 재작성된 통합 코드가 전혀 필요하지 않습니다 (이는 modelcontextprotocol.io가 광고하는 "광범위한 생태계 지원("broad ecosystem support")" 모델입니다).
이 두 가지는 상호 배타적이지 않습니다. Spring AI는 자체적인 MCP 클라이언트 (client) 부트 스타터 (boot starter)를 제공하므로, Spring Boot 에이전트는 위에서 언급한 McpClient를 직접 구현하는 대신 이 서버를 자동 설정된 도구 (tools)로 사용할 수 있습니다:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
...
spring.ai.mcp.client.stdio.servers-configuration을 서버 실행 설정으로 지정하면 되며, 이는 modelcontextprotocol.io의 Build an MCP Server Java 가이드에서도 확인되었습니다. 프레임워크의 도구 추상화 (tool abstractions)와 MCP는 동일한 문제의 서로 다른 계층을 해결합니다. 하나는 메서드를 프롬프트 (prompt)에 연결하고, 다른 하나는 해당 메서드의 _서버 (server)_가 특정 프레임워크의 수명보다 더 오래 지속될 수 있도록 합니다.
MCP가 가치를 발휘하는 때 — 그리고 과잉(Overkill)인 때
도구 또는 데이터 소스가 독립적으로 구축된 여러 AI 애플리케이션 간에 공유되어야 할 때 MCP를 사용하세요. 예를 들어, 고객 지원 직원을 위한 Claude Desktop, 엔지니어를 위한 Claude Code, 그리고 맞춤형 내부 에이전트가 모두 동일한 계정 조회 기능을 사용하는 경우입니다. 또는 호스트마다 통합 코드를 작성하지 않고도 타인의 서버 (Sentry, GitHub, 파일 시스템 등)를 사용할 때도 적합합니다. 도구가 단일 에이전트의 사적인 구현 세부 사항이 아니라, 그 자체로 하나의 제품일 때 MCP를 선택하는 것이 옳습니다.
반면, 도구를 호출하는 주체가 단 하나의 코드베이스에 단 하나뿐이라면 MCP는 불필요한 절차 (ceremony)가 됩니다. 별도의 프로세스, stdio 또는 HTTP 전송 (transport), 그리고 프로토콜 핸드셰이크 (protocol handshake)는 JVM을 벗어나지 않는 @Tool 어노테이션이 붙은 메서드 (포스트 26)나 직접 작성한 Tool 스키마 (post 14)에 비해 순수한 오버헤드일 뿐입니다. 먼저 직접 호출하는 방식을 구축하고, 두 번째 독립적인 호스트가 실제로 이를 호출해야 할 때 비로소 MCP 서버로 격상시키세요.
실무 체크리스트
| 실무 지침 | 중요한 이유 |
|---|---|
| 직접적인 도구 호출로 시작하고 (포스트 14/26), 두 번째 호스트가 필요할 때만 MCP로 격상하세요 | MCP의 비용(프로세스, 전송, 프로토콜)은 호출자가 단 한 명일 때는 아무런 이득을 주지 못합니다 |
| ... |
마치며
마치며
MCP는 14번 포스트의 에이전틱 루프 (agentic loop)나 26번 포스트의 프레임워크 도구 추상화 (framework tool abstractions)를 대체하는 것이 아닙니다. 대신 그 이면에 있는 것, 즉 호스트별 글루 코드 (glue code) 없이도 규약을 준수하는 어떤 클라이언트든 발견하고 호출할 수 있는 도구 서버 (tool server)를 표준화합니다. 이러한 표준화는 하나의 도구가 하나 이상의 애플리케이션에 서비스를 제공해야 할 때 실제 엔지니어링 비용을 들일 만한 가치가 있습니다. 호출자가 단 하나뿐인 도구의 경우에는, 이 시리즈에서 이미 다루었던 직접적인 접근 방식들이 여전히 더 단순하고 올바른 기본값으로 남습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기