MCP + OpenAPI: 단 50줄의 코드로 기존 Spring Boot API를 MCP 서버로 전환하는 방법
요약
기존 Spring Boot API의 OpenAPI 명세를 활용하여 단 50줄의 코드로 MCP(Model Context Protocol) 서버를 구축하는 방법을 소개합니다. 수동으로 MCP 도구를 정의하는 번거로움을 줄이고, 기존 API를 효율적으로 AI 에이전트와 연결하는 자동화 전략을 다룹니다.
핵심 포인트
- OpenAPI 명세의 정보를 활용해 MCP 도구를 자동 변환 가능
- 수동 도구 정의 시 발생하는 중복 및 유지보수 문제 해결
- Spring Boot 프로젝트에 단 50줄의 Java 코드로 MCP 지원 추가
- 기존 REST 엔드포인트를 AI 어시스턴트용 도구로 즉시 전환
MCP + OpenAPI: 단 50줄의 코드로 기존 Spring Boot API를 MCP 서버로 전환하는 방법
솔직히 말해서, 이렇게 잘 작동할 줄은 몰랐습니다.
수년 동안 방치되어 있던 오래된 Spring Boot 프로젝트가 하나 있습니다. MCP라는 개념이 생기기도 전에 구축된 제 지식 베이스(knowledge base)의 오리지널 백엔드입니다. 여기에는 멋진 REST 엔드포인트(endpoints)들이 있고, SpringDoc에 의해 이미 생성된 OpenAPI 문서도 있으며, 모든 것이 잘 작동하고 있습니다. 하지만 제 지식 베이스 실험을 위해 MCP 열풍에 합류하기로 했을 때, 저는 모든 것을 새로 작성해야 할 것이라고 생각했습니다.
"그냥 처음부터 새로운 MCP 서버를 만들자."라고 스스로에게 말했습니다. 다들 그렇게 하잖아요, 그렇죠? 새로 시작하는 거 말입니다.
3시간이 지났을 때, 저는 제 OpenAPI JSON을 빤히 바라보며 생각했습니다. 잠깐만. 왜 내가 모든 엔드포인트를 MCP 도구(tool) 정의로 일일이 수동 복사하고 있는 거지? 내 API는 이미 스스로를 설명하고 있는데! 왜 OpenAPI 명세(specs)를 MCP 도구로 자동 변환할 수는 없는 걸까?
그래서 제가 해냈습니다. 그리고 단 50줄의 Java 코드면 충분했습니다. 오늘은 이것이 어떻게 작동하는지, 제가 무엇을 잘못 생각했는지, 그리고 이것이 기존 API에 MCP 지원을 추가하는 방식에 대해 여러분의 생각을 어떻게 바꿀 수 있는지 보여드리고자 합니다.
문제점: 이미 작동하는 API가 있는데, 왜 다시 작성해야 하는가?
상황을 설명해 보겠습니다. 제 지식 베이스 프로젝트인 Papers는 6년 동안 존재해 왔습니다. 네, 6년입니다. 벌써 세 번이나 다시 작성했습니다. 현재 버전은 완벽하게 잘 작동하는 REST API를 가지고 있습니다:
GET /api/search— 키워드로 노트 검색GET /api/notes/{id}— 특정 노트 가져오기POST /api/notes— 새 노트 생성PUT /api/notes/{id}— 노트 업데이트DELETE /api/notes/{id}— 노트 삭제
이 모든 것은 이미 작동합니다. 테스트도 완료되었고, 배포도 되었으며, 안정적입니다. 심지어 SpringDoc OpenAPI에 의해 자동으로 생성된 완전한 OpenAPI 3.0 문서도 갖추고 있습니다.
AI 어시스턴트가 제 지식 베이스를 사용할 수 있도록 MCP 지원을 추가하기로 했을 때, 저는 다른 사람들이 무엇을 하고 있는지 살펴보았습니다. 대부분의 사람들은 MCP 도구를 수동으로 작성하고 있습니다. 도구 하나당 메서드 하나를 만들고, 입력 스키마(input schema)를 수동으로 정의하고, 모든 것을 연결하는 방식 말입니다.
그건 정말... 지루하게 들리네요. 엔드포인트(endpoint)가 5개라면 큰 문제가 아니겠지만, 만약 50개라면요? 100개라면요? 동일한 API에 대해 두 개의 병렬적인 설명을 정말로 유지하고 싶으신가요?
저는 6년간의 유지보수 경험을 통해 이 사실을 뼈아프게 배웠습니다. 중복은 항상 나쁜 결과를 초래합니다. 하나를 업데이트할 때 다른 하나를 업데이트하는 것을 잊어버리게 됩니다. 무언가 어긋나기 시작하죠. 버그가 발생합니다.
더 나은 방법이 반드시 있어야만 했습니다.
통찰: OpenAPI는 이미 MCP에 필요한 모든 정보를 가지고 있다
이것에 대해 생각해 봅시다. MCP가 도구(tool)로부터 필요로 하는 것은 무엇일까요?
- 도구의 이름
- 도구가 무엇을 하는지에 대한 설명
- 파라미터(parameter)를 설명하는 입력 스키마 (JSON Schema)
- 호출을 실행하고 결과를 반환하는 방법
놀랍게도, OpenAPI는 이미 _이 모든 것_을 가지고 있습니다.
name→ operation ID에서 가져옵니다.description→ operation summary/description에서 가져옵니다.input schema→ OpenAPI는 이미 요청 파라미터(request parameter)와 요청 본문(request body)에 대한 JSON Schema를 가지고 있습니다.execution→ 이미 HTTP 엔드포인트(endpoint)를 가지고 있으니, 요청을 프록시(proxy)하기만 하면 됩니다!
왜 진작 이걸 생각하지 못했을까 믿기지 않을 정도입니다. 너무나 명확한 매핑(mapping)입니다. 그런데 왜 모두가 이렇게 하지 않을까요?
음, 몇 가지 주의할 점(gotchas)은 있습니다. MCP는 도구가 상태가 없고(stateless), 멱등성(idempotent)을 어느 정도 유지하며, 각 도구가 기본적으로 하나의 함수이기를 기대합니다. 이는 REST 리소스(resource)와 정말 잘 매핑됩니다. 즉, 각 작업(operation)이 하나의 도구가 되는 것입니다.
코드를 보여드리겠습니다. 사실 정말 간단합니다.
코드: 작업을 수행하는 50줄의 코드
여기가 핵심입니다. 저는 SpringDoc OpenAPI를 읽어서 모든 작업을 MCP 도구로 자동 노출하는 OpenApiMcpAdapter를 만들었습니다.
먼저 의존성(dependency)입니다. Spring Boot를 사용하고 있다면 아마 이미 가지고 계실 것입니다:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
...
이제 어댑터(adapter) 자체입니다:
@Component
public class OpenApiMcpAdapter {
private final OpenAPI openApi;
...
잠깐 — 사실 이게 거의 전부입니다! 가장 어려웠던 부분은 파라미터 처리(parameter handling)였습니다. 파라미터가 어디로 가는지(경로(path), 쿼리(query), 바디(body))를 추출하고 이를 올바르게 바인딩(binding)하는 것이 가장 까다로웠습니다.
그런 다음 이 코드를 MCP 컨트롤러(controller)에 연결하기만 하면 됩니다:
@RestController
@RequestMapping("/mcp")
public class McpController {
...
그게 끝입니다. 실제 코드는 50줄뿐입니다. 나머지는 모두 제 리포지토리(repo)에서 복사할 수 있는 파라미터 바인딩(parameter binding)일 뿐입니다.
이게 실제로 작동할까요? 직접 보여드리겠습니다.
저는 이것을 기존의 Papers Spring Boot 애플리케이션에 적용하고, 재시작한 뒤 Claude Desktop 클라이언트를 연결했습니다.
작동했습니다. 단 한 번의 시도만에 말이죠.
과장이 아닙니다. 다음과 같은 일이 일어났습니다:
- Claude Desktop이 제 MCP 서버에 연결합니다.
/mcp/tools/list를 호출합니다.- 제 어댑터(adapter)가 기존 OpenAPI로부터 도구(tool) 정의를 자동으로 생성합니다.
- Claude는 다섯 개의 도구를 확인합니다:
searchNotes,getNote,createNote,updateNote,deleteNote - 제가 Claude에게 묻습니다: "MCP 서버 설계에 관한 내 모든 노트를 찾아줘"
- Claude는
query: "MCP server design"과 함께searchNotes도구를 호출합니다. - 제 어댑터가 이를 기존의
GET /api/search엔드포인트(endpoint)로 프록시(proxy)합니다. - 결과가 돌아오고, Claude가 저를 위해 이를 요약합니다.
이것이 전체 흐름입니다. 수동 도구 정의가 전혀 필요 없습니다. 중복도 전혀 없습니다. 기존 API가 그대로 MCP 도구로서 작동합니다.
너무 쉬워서 믿기지 않았기 때문에 저는 5분 동안 멍하니 화면을 바라보았습니다. 6년 동안 모든 것을 새로 작성해 온 저에게, 가장 게으른 방식이 실제로 통했습니다.
제가 실수했던 부분: 놀라운 주의사항들
좋습니다, 솔직해집시다. 모든 것이 완벽하지는 않습니다. 여러분이 이것을 시도해보고 싶다면 알아두어야 할 몇 가지 놀라운 문제들을 겪었습니다.
1. 모든 작업(operation)이 좋은 도구가 되지는 않습니다
어떤 작업들은 매핑(mapping)이 잘 되지 않습니다. 예를 들어, page와 size 파라미터를 기대하는 페이지네이션(pagination) 목록 엔드포인트가 있다면 괜찮습니다. 하지만 /users/{userId}/posts/{postId}/comments/{commentId}와 같이 여러 경로 파라미터(path parameter)를 가진 복잡한 중첩 리소스(nested resource)가 있는 경우, 작동은 하지만 입력 스키마(input schema)가 지저분해집니다.
솔직히 말해서요? MCP 클라이언트(clients)는 이를 잘 처리합니다. 그들은 그냥 파라미터(parameters)를 전달할 뿐입니다. 제대로 작동합니다.
2. Operation ID는 고유하고(Unique) 합리적이어야 합니다
SpringDoc은 기본적으로 getById와 같은 operation ID를 생성합니다. 이로 인해 서로 다른 경로(paths)에서 동일한 이름을 가진 여러 operation이 생기게 됩니다. 전체 API에 걸쳐 operation ID가 고유하도록 보장해야 합니다.
저는 이 문제를 아주 어렵게 배웠습니다. "getNote"가 GET /api/notes/{id}가 아닌 GET /api/users/{id}에 매핑되는 이상한 오류를 겪었거든요. 이런.
SpringDoc이 완전 정규화된(fully qualified) operation ID를 사용하도록 설정하여 해결하세요:
springdoc.operation-id-generator=io.swagger.v3.oas.annotations.media.ArraySchemadependsOn
# 또는 org.springdoc.core.customizers.OperationIdCustomizer를 사용하여 접두사(prefixes)를 추가하세요
3. 인증(Authentication)이 까다로워집니다
API에 인증이 필요한 경우, 이제 두 개의 계층이 생깁니다. MCP 클라이언트가 MCP 서버에 인증하고, 그다음 MCP 서버가 API에 인증해야 합니다.
제 경우에는 개인 서버이기 때문에, MCP 클라이언트로부터 받은 API 키를 백엔드로 그대로 전달(pass through)합니다. 프로덕션(production) 환경이라면 이를 더 주의 깊게 처리해야 하겠지만, 이는 자동 생성 방식을 사용하든 직접 작성하든 마찬가지인 사실입니다.
4. Content-Types가 혼란스러울 수 있습니다
일부 OpenAPI 명세(specs)는 요청 본문(request bodies)에 대해 여러 가지 콘텐츠 타입(content types)을 설명합니다. MCP는 거의 항상 JSON을 전송하므로, JSON 스키마(JSON schema)를 선택하기만 하면 됩니다. 대부분의 경우 문제없이 작동합니다.
장단점: 이 방식을 사용해야 할까요?
솔직하게 말씀드리겠습니다. 이 접근 방식이 모든 사람에게 적합한 것은 아닙니다. 다음과 같이 요약할 수 있습니다:
장점 ✅
- 중복 제로 (Zero duplication) — 여러분의 OpenAPI가 단일 진실 공급원 (Single source of truth)이 됩니다. API를 업데이트하면 MCP 도구(Tools)도 자동으로 업데이트됩니다. 추가 작업이 필요 없습니다.
- MCP 지원 추가가 매우 빠름 — 저는 0에서 시작해 3시간 만에 작동하는 MCP 통합을 구현했습니다. 그 시간의 대부분은 코드를 작성하는 것이 아니라 매핑(Mapping) 방식을 파악하는 데 쓰였습니다.
- 기존 자산 활용 — 이미 OpenAPI가 포함된 작동하는 API를 가지고 있다면, 왜 다시 작성하겠습니까?
- OpenAPI를 지원하는 모든 프레임워크와 호환 — 이것은 Spring Boot에 국한된 것이 아닙니다. OpenAPI JSON을 출력할 수 있는 모든 API, 즉 Node.js, Python 등 무엇이든 작동합니다. 작은 어댑터(Adapter)만 있으면 됩니다.
- 점진적 도입 — 모든 것을 한꺼번에 전환할 필요는 없습니다. 자동 생성된 도구와 복잡한 케이스를 위한 수동 작성 도구를 혼합하여 사용할 수 있습니다.
단점 ❌
- 도구 설명에 대한 제어력 감소 — OpenAPI의 설명(Description)이 좋아야 합니다. 만약 OpenAPI 문서가 형편없다면, MCP 도구의 설명도 형편없을 것입니다. 쓰레기를 넣으면 쓰레기가 나옵니다 (Garbage in, garbage out).
- 모든 작업(Operation)이 좋은 도구가 되는 것은 아님 — 어떤 작업들은 MCP 도구 모델에 맞지 않습니다 (파일 업로드, 스트리밍 응답 등). 이러한 것들은 필터링해야 합니다.
- 추가적인 프록시 홉 (Proxy hop) — MCP 엔드포인트에서 기존 엔드포인트로 프록시를 거치게 됩니다. 실제로 이는 몇 밀리초(ms)를 추가합니다. 대부분의 사용 사례에서는 완전히 무시할 수 있는 수준입니다.
- OpenAPI 명세(Spec) 품질의 가변성 — 일부 생성기(Generator)는 이상한 OpenAPI 출력을 만들어냅니다. 먼저 명세를 정리해야 할 수도 있습니다.
그렇다면 언제 이 방식을 사용해야 할까요?
이럴 때 사용하세요:
- 이미 OpenAPI가 있는 기존 REST API를 보유하고 있는 경우
- MCP 지원을 빠르게 추가하고 싶은 경우
- 저만큼이나 중복을 싫어하는 경우
- 개인용 또는 내부 도구를 구축하고 있는 경우
이럴 때는 사용하지 마세요:
- 처음부터 공개용 MCP 서비스를 구축하며 완벽한 도구 설명을 원하는 경우
- 커스텀 오케스트레이션 (Custom orchestration)이 필요한 매우 복잡한 워크플로우를 가진 경우
- OpenAPI 문서가 아예 없거나 형편없는 경우
실제 결과: 2주간의 경험 후기
저는 이 자동 변환된 설정을 개인 지식 베이스(knowledge base)에 2주 동안 적용해 왔는데, 솔직히 말씀드리면 기대했던 것보다 훨씬 더 잘 작동합니다.
Claude는 자동 생성된 도구(tools)를 사용하는 데 아무런 문제가 없습니다. 저는 이미 OpenAPI 문서를 괜찮은 수준으로 유지하고 있기 때문에 설명(descriptions)도 충분히 훌륭합니다. API에 새로운 엔드포인트(endpoint)를 추가하면, 자동으로 MCP 도구로 나타납니다. 제가 따로 할 일은 아무것도 없습니다.
정말 놀라운 점은 무엇일까요? 이것은 도구를 직접 손으로 작성하는 것보다 유지보수(maintainable)하기가 더 쉽다는 것입니다. 어차피 저는 제 API를 위해 OpenAPI 문서를 이미 유지보수하고 있기 때문입니다. 추가적인 작업을 하는 것이 아니라, 이미 가지고 있는 것을 재사용하는 것뿐입니다.
저는 지식 베이스를 위해 도구들을 직접 손으로 작성하면서 이 MCP 여정을 시작했습니다. 당시에는 5개의 도구를 위해 150줄의 코드가 필요했습니다. 이제는 어떠한 수의 도구라도 자동으로 처리할 수 있는 총 50줄의 코드만 있으면 됩니다. 제 기준에서는 완벽한 승리입니다.
큰 그림: MCP는 바퀴를 재발명하는 것이 아니라 상호 운용성에 관한 것입니다
최근 제가 하고 있는 생각은 이렇습니다. 모두가 새로운 MCP 네이티브(MCP-native) 서비스를 구축하려고 서두르고 있지만, 우리 대부분은 이미 작동하는 API를 가지고 있습니다. 왜 MCP를 위해 모든 것을 다시 구축해야 할까요?
MCP는 상호 운용성(interoperability)을 위한 프로토콜입니다. 기존의 모든 서비스를 다시 작성하도록 요구해서는 안 됩니다. 이미 작업을 수행하는 API가 있다면, 최소한의 글루 코드(glue code)만으로 MCP를 통해 노출할 수 있어야 합니다.
이 방식이 바로 그 역할을 수행합니다. 이것은 단지 두 가지 기존 표준, 즉 모두가 이미 사용하고 있는 OpenAPI와 새로운 AI 도구 프로토콜인 MCP 사이를 이어주는 글루(glue)일 뿐입니다. 표준들이 함께 작동하는 것, 그것이 바로 원래 의도된 방식 아닌가요?
저는 최고의 코드는 작성할 필요가 없는 코드라는 것을 알 정도로 충분히 오래 이 일을 해왔습니다. 이 접근 방식은 코드를 거의 작성하지 않고도 기존 API에 MCP 지원을 추가할 수 있게 해줍니다.
직접 시도해 보세요
전체 작동 코드를 확인하고 싶다면, GitHub에 있는 제 프로젝트 Papers를 확인해 보세요. OpenAPI 어댑터(adapter)가 포함되어 있어 바로 포크(fork)하여 사용할 수 있습니다:
https://github.com/kevinten10/Papers
모든 파라미터 처리를 포함해도 어댑터의 전체 길이는 약 150줄 정도입니다. 대부분은 예외적인 케이스(edge cases)를 처리하는 코드입니다. 핵심 아이디어는 50줄 내외입니다.
여러분의 생각은 어떠신가요?
저는 진심으로 궁금합니다. 혹시 MCP 도구(tools)로 노출하고 싶은 기존의 OpenAPI 기반 API를 가지고 계신가요? 이 방식이 여러분에게 유용할까요, 아니면 제가 놓치고 있는 중요한 부분이 있을까요?
저는 지난 몇 주 동안 개인 프로젝트에만 이 방식을 사용해 왔습니다. 여러분이 직접 시도해 보신다면 그 경험이 어떠했는지 꼭 듣고 싶습니다. OpenAPI로부터 MCP 도구를 자동 생성하는 것이 좋은 아이디어라고 생각하시나요, 아니면 모든 도구를 AI를 위해 수동으로 제작(hand-crafted)해야 한다고 생각하시나요?
아래에 댓글을 남겨 의견을 알려주세요!
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기