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가 여전히 더 적절한 선택인 일반적인 서비스 간 통신에는 사용하지 마세요.

mcpspring-aillmjavaai-integration