MCP(Model Context Protocol)란 무엇인가? Java 개발자를 위한 해설
1. 개요
MCP(Model Context Protocol)는 각 AI 애플리케이션이 각 데이터 소스마다 독립적으로 통합을 작성하는 대신, LLM(large language model)이 데이터, 도구, 외부 시스템과 통일된 방식으로 연결할 수 있게 하는 개방형 표준입니다. 이 글에서는 MCP가 무엇인지, 그 아키텍처가 어떻게 구성되는지 설명하고, LLM이 호출할 수 있는 도구를 제공하기 위해 Spring Boot(Spring AI를 통해)로 간단한 MCP 서버를 작성하는 방법을 안내합니다. 이 글을 읽은 후에는 MCP를 언제 사용해야 하는지, 언제 과도한 엔지니어링인지 이해하게 될 것입니다.
2. MCP가 해결하는 문제
MCP 이전에는 각 AI 애플리케이션(챗봇, 에이전트)이 데이터베이스, 파일 시스템, 내부 API 등에 접근하려면 각각 어댑터를 직접 작성해야 했습니다. M개의 AI 애플리케이션과 N개의 데이터 소스가 있다면, M × N개의 서로 다른 통합 코드가 필요했습니다.
MCP는 이 통신 계층을 표준화합니다: AI 애플리케이션(Host라고 함)이 MCP 표준을 한 번만 구현하면, MCP 서버를 제공하는 모든 시스템과 ’대화’할 수 있습니다. 이는 전자기기의 USB-C처럼 수십 가지 다양한 포트를 대체하는 하나의 표준화된 커넥터와 같습니다.
주의: MCP는 일반적인 서비스 간 통신을 위해 REST나 gRPC를 대체하는 프레임워크가 아닙니다. LLM/에이전트가 도구를 호출하기 위해 설계되었으며, 두 개의 백엔드 마이크로서비스가 서로 통신하기 위한 것이 아닙니다.
3. MCP 아키텍처
MCP는 3가지 주요 역할을 가집니다:
- Host: LLM을 포함하는 애플리케이션(예: Claude Desktop, 직접 작성한 에이전트)
- Client: Host 내의 컴포넌트로, 하나의 MCP 서버와 1:1 연결을 유지합니다.
- Server: MCP 표준에 따라 기능(도구, 리소스, 프롬프트)을 제공하는 프로세스입니다.
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Host │ 1-1 │ MCP Client │ JSON-RPC │ MCP Server │
│ (LLM 포함) │◄──────►│ │◄──────────►│ (DB, API, file) │
└──────────────┘ └──────────────┘ └──────────────────┘
클라이언트와 서버 간의 통신은 JSON-RPC 2.0을 사용하며, 두 가지 일반적인 전송 방식 중 하나로 전달됩니다:
- stdio: 서버는 자식 프로세스로 실행되고, 클라이언트/서버는 stdin/stdout으로 통신합니다. 서버가 로컬에서 실행될 때 적합합니다.
- SSE/HTTP(Streamable HTTP): 서버가 원격에서 실행되고 네트워크를 통해 통신할 때 사용됩니다.
3.1. MCP 서버가 제공할 수 있는 세 가지 기능 유형
| 기능 유형 | 의미 | 예시 |
|---|---|---|
| Tools | LLM이 호출하여 작업을 수행할 수 있는 함수 | getWeather(city), createOrder(...) |
| Resources | LLM이 읽을 수 있는 데이터(GET과 유사) | 로그 파일, 데이터베이스 레코드 |
| Prompts | 서버에서 미리 정의한 프롬프트 템플릿 | “주문을 패턴 X에 따라 요약하기” |
대부분의 실제 사용 사례는 Tools 중심이므로, 아래 예제도 이에 집중합니다.
4. Spring AI로 MCP 서버 작성하기
Spring AI(1.0.0-M6부터)는 전용 스타터를 통해 MCP를 지원하므로, 어노테이션을 사용하여 Java 빈을 MCP 도구로 제공할 수 있으며 JSON-RPC를 직접 구현할 필요가 없습니다.
4.1. 의존성 추가
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
4.2. 도구 정의
@Service
public class WeatherService {
@Tool(description = "특정 도시의 현재 온도를 가져옵니다")
public String getTemperature(String city) {
return switch (city.toLowerCase()) {
case "ha noi" -> "28°C, 구름 많음";
case "sai gon" -> "32°C, 맑음";
default -> "해당 도시의 데이터가 없습니다: " + city;
};
}
}
@Tool은 이 메서드를 MCP 도구로 표시합니다.description속성은 매우 중요합니다 — LLM은 이 설명을 사용하여 도구를 호출할지 여부와 어떤 매개변수를 사용할지 결정합니다.getTemperature는String city를 받아 날씨 설명 문자열을 반환합니다. 매개변수와 반환 타입은 MCP 서버에서 자동으로 JSON 스키마를 생성하여 LLM이 이해할 수 있도록 합니다.
4.3. MCP 서버에 도구 등록하기
@Configuration
public class McpServerConfig {
@Bean
public ToolCallbackProvider weatherTools(WeatherService weatherService) {
return MethodToolCallbackProvider.builder()
.toolObjects(weatherService)
.build();
}
}
MethodToolCallbackProvider는 weatherService에서 @Tool 어노테이션이 있는 메서드를 스캔하고, 애플리케이션 컨텍스트가 시작될 때 자동으로 MCP 서버에 등록합니다.
4.4. 서버 구성
spring.ai.mcp.server.name=weather-mcp-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.stdio=true
stdio=true를 사용하면 서버는 독립적인 프로세스로 실행되어 stdin/stdout으로 통신합니다 — Claude Desktop 또는 직접 작성한 MCP 클라이언트로 빠르게 테스트하기에 적합합니다. 서버를 원격에서 실행되는 독립적인 서비스로 배포하려면 SSE 전송으로 전환하고 stdio 대신 HTTP 엔드포인트를 통해 제공합니다.
서버를 시작합니다:
mvn spring-boot:run
Started WeatherMcpApplication in 1.124 seconds
Registered MCP tool: getTemperature
5. 클라이언트에서 MCP 서버 호출하기
위의 getTemperature 도구를 호출해야 하는 Host 애플리케이션(예: 내부 에이전트)을 작성한다고 가정합시다.
5.1. 클라이언트 의존성
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
5.2. 도구 호출
@Component
public class WeatherAgent {
private final McpSyncClient mcpClient;
public WeatherAgent(McpSyncClient mcpClient) {
this.mcpClient = mcpClient;
}
public String askTemperature(String city) {
CallToolRequest request = new CallToolRequest(
"getTemperature",
Map.of("city", city));
CallToolResult result = mcpClient.callTool(request);
return result.content().get(0).text();
}
}
McpSyncClient은application.yml에서 선언한 전송 구성(stdio 또는 SSE)을 기반으로 Spring AI에서 자동으로 생성하는 클라이언트 빈입니다.CallToolRequest는 도구 이름(getTemperature)과 매개변수(city)를 JSON-RPC 호출로 패키징합니다.result.content()는 응답 콘텐츠 목록을 반환합니다. 여기서는 첫 번째text부분을 가져옵니다.
클라이언트 구성(예: 서버 jar를 서브프로세스로 실행하여 stdio를 통해 서버에 연결):
spring:
ai:
mcp:
client:
stdio:
connections:
weather-server:
command: java
args: -jar weather-mcp-server.jar
6. JUnit 5 + AssertJ로 도구 테스트하기
@Tool은 단순한 Java 메서드 어노테이션이므로, MCP와 독립적으로 비즈니스 로직을 테스트할 수 있습니다 — 전체 서버를 구축할 필요가 없습니다:
class WeatherServiceTest {
private final WeatherService weatherService = new WeatherService();
@Test
void shouldReturnTemperatureForKnownCity() {
String result = weatherService.getTemperature("Ha Noi");
assertThat(result).contains("28°C");
}
@Test
void shouldReturnFallbackMessageForUnknownCity() {
String result = weatherService.getTemperature("Tokyo");
assertThat(result)
.startsWith("해당 도시의 데이터가 없습니다:")
.contains("Tokyo");
}
}
MCP 통합 부분도 테스트하고 싶다면(도구가 올바른 이름과 스키마로 등록되는지), @SpringBootTest 수준에서 테스트를 작성하고 ToolCallbackProvider를 확인할 수 있습니다:
@SpringBootTest
class McpServerConfigTest {
@Autowired
private ToolCallbackProvider toolCallbackProvider;
@Test
void shouldExposeTemperatureTool() {
var toolNames = toolCallbackProvider.getToolCallbacks().stream()
.map(callback -> callback.getToolDefinition().name())
.toList();
assertThat(toolNames).contains("getTemperature");
}
}
이 테스트는 WeatherService를 리팩토링할 때마다 도구가 올바른 이름으로 제공되도록 보장합니다 — LLM이 참조하는 도구의 이름을 변경했지만 업데이트를 잊는 경우를 방지합니다.
7. 흔한 오류와 MCP를 사용하지 말아야 할 경우
도구 설명이 너무 모호합니다. description이 ’데이터 가져오기’만 쓰여 있으면, 유사한 도구가 많을 때 LLM이 잘못된 도구를 선택하기 쉽습니다. 입력/출력이 무엇인지 명확하게 작성하세요.
내부 REST/gRPC를 대체하기 위해 MCP를 사용합니다. MCP는 추가 JSON-RPC 계층과 전송 방식을 추가하며, LLM이 동적으로 도구를 호출하는 컨텍스트에 최적화됩니다 — 두 백엔드 서비스가 높은 빈도, 낮은 지연 시간으로 서로 호출하는 경우에는 최적화되지 않습니다. 이 경우 MCP를 사용하는 것은 과도한 엔지니어링입니다.
도구의 권한을 제어하지 않습니다. MCP 서버는 일반적으로 호스트 프로세스의 권한으로 실행됩니다. 입력 매개변수를 제한하지 않고 파일을 삭제하거나 셸 명령을 실행할 수 있는 도구를 제공하면, LLM(또는 프롬프트 주입)이 해당 도구를 악용할 수 있습니다. 항상 도구의 매개변수를 검증하고 LLM의 입력을 맹목적으로 신뢰하지 마세요.
서버리스 환경에서 stdio 서버를 실행합니다. stdio 전송은 자식 프로세스를 생성해야 합니다 — 서버리스/단기 컨테이너 환경에서는 잘 작동하지 않습니다. 이 경우 SSE/HTTP 전송을 사용해야 합니다.
클라이언트와 서버 간 버전을 동기화하지 않습니다. MCP는 여전히 빠르게 개발 중입니다(사양 버전 간 많은 breaking changes). SDK를 업그레이드할 때는 프로덕션 배포 전에 변경 로그를 자세히 확인하세요.
8. 결론
MCP는 LLM이 도구를 호출하고 외부 데이터를 읽는 방식을 표준화하여, AI 애플리케이션과 데이터 소스 간의 M × N 통합 문제를 해결합니다. Spring AI를 사용하면 @Tool 어노테이션만으로 Java 서비스를 MCP 도구로 제공할 수 있으며 JSON-RPC를 직접 구현할 필요가 없습니다. LLM의 결정에 따라 도구를 동적으로 호출해야 하는 에이전트/챗봇을 구축할 때 MCP를 사용하세요 — REST나 gRPC가 여전히 더 적절한 선택인 일반적인 서비스 간 통신에는 사용하지 마세요.