본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 25. 15:31

MCP 베스트 프랙티스 체크리스트: 77개의 글을 작성한 후, 10개 이상의 MCP 서버를 구축하기 전에 알았더라면 좋았을 것들

요약

MCP(Model Context Protocol) 서버를 구축하며 겪은 시행착오를 바탕으로 작성된 베스트 프랙티스 가이드입니다. 특히 다양한 클라이언트 환경에서의 인증 호환성 문제와 CORS 설정 오류를 방지하기 위한 실무적인 팁을 제공합니다.

핵심 포인트

  • 다양한 MCP 클라이언트의 API 키 전달 방식(헤더, 쿼리 스트링 등)을 모두 지원해야 함
  • CORS 대응을 위해 OPTIONS 프리플라이트 요청 시 인증 과정을 건너뛰어야 함
  • 클라이언트 간의 호환성을 확보하는 것이 표준 준수보다 실무적으로 중요함

MCP 베스트 프랙티스 체크리스트: 77개의 글을 작성한 후, 10개 이상의 MCP 서버를 구축하기 전에 알았더라면 좋았을 것들

솔직히 말해서, 제 프로젝트들을 위해 MCP 서버를 구축하기 시작한 지 거의 3개월이 지났다는 게 믿기지 않습니다. 그리고 제가 이에 대해 얼마나 많은 글을 썼는지 세어본다면... 음, 무려 77개의 글입니다. 네, 77개요. 제가 좀 과했을지도 모릅니다. 하지만 77번이나 서로 다른 방식으로 고생하며 무언가를 배우다 보면, 패턴이 보이기 시작합니다.

잠시 뒤로 돌아가 보죠. 저는 Papers라고 불리는 저의 1,847시간짜리 지식 베이스 프로젝트를 위해 첫 번째 MCP 서버를 구축했습니다. 들어본 적이 없으시다면, 이건 제가 6년 동안 작업해 온 사이드 프로젝트인데, 기본적으로 "놀라운 아이디어"에서 "99.4% ROI 손실"을 거쳐 "MCP를 통해 재탄생"한 프로젝트입니다. 이건 꽤 긴 이야기이니, 실패담(failure porn)에 관심이 있다면 다른 글들을 읽어보셔도 좋습니다.

요점은 이렇습니다. Papers, Capa-Java, Spatial Memory, AI-Tools를 위해 MCP 서버를 구축한 후... 발생 가능한 모든 에러를 겪고, CORS와 싸우고, 인증(authentication)을 디버깅하고, 타임아웃(timeout)을 기다리느라 10초를 허비하고, 3개월간의 배포(deployment) 두통을 겪은 끝에... 저는 체크리스트를 정리했습니다. 그리고 오늘 그것을 여러분과 공유하고자 합니다.

군더더기 없이, 여러분이 구글링하며 시간을 허비하거나 커피를 마시며 눈물 흘리는 시간을 줄여줄 내용들만 담았습니다. 시작해 보죠.

1. 인증 (Authentication): 네 가지 API 키 위치를 모두 지원하라

저는 이것을 아주 어렵게 배웠습니다. 왜 제 MCP 서버가 Claude Desktop에서는 작동하는데 Cursor에서는 작동하지 않는지, 그리고 왜 Cursor에서는 작동하는데 제가 테스트 중인 다른 클라이언트에서는 작동하지 않는지 궁금해하며 사흘을 보냈습니다.

알고 보니... 서로 다른 MCP 클라이언트들이 API 키를 서로 다른 위치에 넣고 있었습니다:

  • 어떤 곳은 X-API-Key 헤더를 사용합니다.
  • 어떤 곳은 Authorization: Bearer {key}를 사용합니다.
  • 어떤 곳은 쿼리 스트링(query string)에 api_key로 넣습니다.
  • 어떤 곳은 api_key (snake_case) 대신 apiKey (camelCase)를 사용합니다.

만약 여러분이 단 하나만 지원한다면, 누군가의 환경을 반드시 망가뜨리게 될 것입니다. 사용자들은 여러분의 "표준"에는 관심이 없습니다. 그들은 그저 작동하기만을 원할 뿐입니다.

현재 제가 Java Spring Boot에서 사용하는 방식은 다음과 같습니다:

@Component
public class McpAuthFilter implements Filter {

...

한 가지 더 중요한 점: CORS를 위한 OPTIONS 프리플라이트 (preflight) 요청에는 인증 정보가 포함되지 않습니다. 따라서 OPTIONS 요청에 대해서는 인증을 건너뛰어야 합니다:

// CORS 설정 또는 필터 내에서
if ("OPTIONS".equalsIgnoreCase(req.getMethod())) {
    chain.doFilter(request, response);
...

제 말을 믿으세요. 이 부분을 잊어버리면, 모든 것이 올바르게 보이는데도 왜 CORS가 작동하지 않는지 고민하며 반나절을 허비하게 될 것입니다.

이 방식의 장점: 클라이언트가 키를 어디에 두든 상관없이 어떤 클라이언트라도 연결할 수 있습니다.

단점: 일부 서버에서는 쿼리 파라미터 (Query parameters)를 로그에 기록하므로 보안성이 다소 떨어집니다.

결론: 호환성이 순수성보다 중요합니다. 공개 서비스를 운영 중이라면 여전히 헤더 (headers)를 사용하세요. 하지만 본인만 사용하는 개인용 MCP 서버라면 괜찮습니다.

2. 에러 핸들링 (Error Handling): 절대 빈 응답을 반환하지 마세요

이것은 제가 고생하며 배운 또 다른 교훈입니다. 도구 (tools)는 실패할 수 있습니다. 괜찮습니다. 하지만 무언가 잘못되었을 때 빈 응답을 반환하면... 클라이언트는 그냥 멈춰버립니다. 영원히 말이죠. 혹은 타임아웃 (timeout)이 발생할 때까지요. 디버깅 중일 때는 그 시간이 영원처럼 느껴집니다.

결과가 없을 때라도, 무언가 잘못되었을 때라도, 항상 사람이 읽을 수 있는 에러 메시지를 반환하세요.

나쁜 예:

{
  "content": []
}

좋은 예:

{
  "content": [
    {
...

실제 에러의 경우도 마찬가지입니다:

try {
    // 위험한 작업 수행
    return successResult();
...

또 다른 전문가용 팁: (10초 이상 걸릴 수 있는 콜드 스타트 (cold start)와 같이) 느린 작업의 경우, 프레임워크가 허용한다면 버퍼 (buffer)를 조기에 플러시 (flush) 하세요. 이렇게 하면 연결이 타임아웃되는 것을 방지할 수 있습니다:

// Spring Boot에서는 writer를 조기에 flush 할 수 있습니다
response.getWriter().flush();

3. 프레임워크가 JSON을 처리하게 하세요. 절대 수동으로 만들지 마세요.

초기 MCP 예제들에서 이런 방식을 보고 저도 직접 시도해 본 적이 있습니다. 그러지 마세요.

이것은 범죄입니다:

// 이렇게 하지 마세요
String json = "{\"content\": [{\"type\": \"text\", \"text\": \"" + userInput + "\"}]}";

이스케이프(escape) 처리되지 않은 따옴표 하나, 이스케이프가 필요한 백슬래시() 하나만 있어도 전체 응답은 유효하지 않은 JSON이 됩니다. 클라이언트는 멈추거나 충돌할 것이며, 당신은 "브라우저에서는 올바르게 보이는데" 왜 그런지 알아내기 위해 한 시간을 허비하게 될 것입니다.

JSON 라이브러리가 직렬화 (serialization)를 처리하도록 하세요. 항상 말이죠. 아주 단순한 응답일지라도 말입니다.

Jackson을 사용하는 Java의 경우:

// 클래스를 한 번 정의해 두세요
public record McpResponseContent(String type, String text) {}
public record McpResponse(List<McpResponseContent> content) {}
...

몇 줄의 코드가 추가되지만, 다시는 JSON 이스케이프 (escaping) 오류를 겪지 않게 될 것입니다. 그만한 가치가 있습니다. 100배 이상의 가치가 있습니다.

4. CORS: 반드시 필요합니다. 작동하는 설정 예시를 확인하세요.

웹 기반 클라이언트가 접속하는 MCP 서버를 운영한다면... CORS (Cross-Origin Resource Sharing)를 올바르게 설정해야 합니다. 그리고 여기서 "올바르게"라는 의미는 당신이 생각하는 것과는 다릅니다.

가장 중요한 사항:

  • OPTIONS 메서드를 허용할 것
  • 클라이언트가 보내는 헤더를 허용할 것 (Content-Type, Authorization, X-API-Key)
  • 클라이언트가 읽기를 원하는 헤더를 노출 (Expose)할 것
  • 쿠키를 사용하는 경우 Credentials 허용

제가 현재 어디에서나 사용하고 있는 작동하는 Spring Boot CORS 설정입니다:

@Configuration
public class CorsConfig implements WebMvcConfigurer {

...

Node.js Express의 경우:

const cors = require('cors');
app.use(cors({
  origin: '*', // 프로덕션 환경에서는 제한하세요
...

그리고 앞서 말한 내용을 기억하세요 — OPTIONS 요청에서는 인증 (auth)을 건너뛰세요. CORS가 당신에게 고마워할 것입니다.

5. 배포 (Deployment): 단순하게 시작하고, 필요할 때 복잡성을 추가하세요

저는 지금까지 Heroku, Fly.io, Tencent Cloud Serverless, VPS 등 다섯 가지 서로 다른 플랫폼에 MCP 서버를 배포해 보았고, 개발용으로 ngrok도 사용해 보았습니다. 제가 말씀드릴 수 있는 것은 다음과 같습니다:

개발용: ngrok을 사용하세요. 비상업적 용도로는 무료이며, 명령어 하나면 됩니다:

ngrok http 8080

끝입니다. 이제 MCP 클라이언트가 접속할 수 있는 공개 HTTPS URL을 갖게 되었습니다. 이보다 더 좋을 순 없습니다.

개인용 24/7 프로젝트의 경우: 현재 제가 가장 선호하는 것은 Fly.io입니다. fly launch 명령어가 모든 과정을 안내하며, 자동 HTTPS, 필요한 경우 지속성 볼륨 (persistent volumes)을 제공하고, 소규모 프로젝트의 경우 월 2~5달러 정도의 비용이 듭니다. 예상치 못한 추가 비용이 없습니다.

트래픽이 적은 개인용 프로젝트의 경우: 서버리스 (Serverless)가 매우 효과적입니다. 비용이 저렴하며 (낮은 사용량에서는 종종 무료임), 콜드 스타트 (cold starts)를 주의해야 합니다. 사용자가 콜드 스타트로 인해 10초를 기다려야 한다면, 개인적인 용도로는 괜찮지만 공개 앱으로서는 좋지 않습니다.

여러 서비스를 운영하는 경우: VPS는 완전한 제어권을 제공하지만, 직접 유지 관리해야 합니다. 비용이 더 많이 들고 작업량도 많습니다. 반드시 필요한 경우에만 이 방식을 선택하세요.

배포 위치와 상관없는 전문가 팁: 200 OK를 반환하는 헬스 체크 (health check) 엔드포인트를 추가하세요:

@RestController
public class HealthController {
    @GetMapping("/health")
...

배포 플랫폼이 이를 핑 (ping) 하여 앱이 여전히 살아있는지 확인할 수 있습니다. 일부 플랫폼은 이를 요구하기도 합니다. 요구하지 않더라도 디버깅에 매우 유용합니다.

6. Content-Length: 내용이 잘린다면, 명시적으로 설정하세요

이 문제는 디버깅하는 데 시간이 꽤 걸렸습니다. 일부 MCP 클라이언트는 청크 인코딩 (chunked encoding)을 제대로 처리하지 못합니다. 만약 응답이 중간에 잘린다면... Content-Length 헤더를 명시적으로 설정하세요.

Java Spring Boot에서는 다음과 같이 할 수 있습니다:

byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
response.setContentLength(jsonBytes.length);
response.getOutputStream().write(jsonBytes);

문자열을 직접 쓰는 대신, 먼저 길이를 계산하고 헤더를 설정한 다음 바이트 (bytes)를 쓰세요. 이렇게 하면 청크 인코딩을 피할 수 있어 모두가 만족하게 됩니다.

몇 줄의 코드가 추가되지만, 응답이 잘리는 문제로 어려움을 겪고 있다면 이 방법이 99%의 경우 해결책이 될 것입니다.

7. 아키텍처: 단순하게 유지하세요. 어차피 힘든 일은 AI가 합니다.

이것이 가장 중요한 부분입니다. 제가 1,847시간과 6년이라는 시간을 들여 배운 교훈입니다.

MCP를 사용하기 전, 저는 다음과 같은 복잡한 지식 베이스 (knowledge base)를 구축하는 데 수년을 보냈습니다:

  • 시맨틱 검색 (Semantic search)
  • 임베딩 생성 (Embeddings generation)
  • 분류 모델 (Classification models)
  • 자동 태깅 (Automatic tagging)
  • 커스텀 랭킹 알고리즘 (Custom ranking algorithms)

이 모든 것이 지식 베이스 (knowledge base) 자체에 포함되어 있었습니다. 수천 줄의 코드였죠.

MCP를 사용한 후에는요? 총 150줄에 불과합니다. 저는 단 세 가지 도구 (tools)만 노출합니다:

  • search_knowledge: 쿼리를 입력받아 일치하는 텍스트 스니펫 (snippets)을 반환
  • get_note: 노트 ID를 입력받아 전체 노트를 반환
  • search_notes_by_tag: 태그를 입력받아 해당 태그가 포함된 노트를 반환

그게 전부입니다. 이제는 AI가 모든 힘든 일을 처리합니다. AI가 쿼리를 이해하고, 결과를 랭킹 매기며, 답변을 합성합니다. 제 지식 베이스는 표준 인터페이스를 통해 데이터만 제공할 뿐입니다.

솔직히 말해서, 복잡했던 이전 버전보다 훨씬 낫습니다. 더 빠르고, 더 단순하며, 오류가 적고, 모든 MCP 호환 AI 클라이언트 (AI client)에서 작동합니다.

만약 지금 MCP 서버를 구축하고 있다면, 스스로에게 물어보세요: "이 로직이 내 서버에 꼭 필요한가, 아니면 AI가 처리할 수 있는가?"

열에 아홉은 AI가 처리할 수 있습니다. 서버를 단순하게 유지하세요.

MCP 서버를 배포하기 전 나의 개인적인 체크리스트

저는 이제 매번 이 과정을 거칩니다. 시간을 정말 많이 아껴줍니다:

  • 인증 (Auth): X-API-Key / Authorization / api_key / apiKey 지원 ✓
  • CORS: OPTIONS 요청은 인증을 건너뛰며, 허용된 헤더 (allowed headers)에 위 세 가지 인증 헤더가 포함됨 ✓
  • 에러 핸들링 (Error handling): 빈 응답을 보내지 않음 — "결과 없음"조차도 사람이 읽을 수 있는 텍스트를 포함함 ✓
  • JSON: 모든 직렬화 (serialization)는 라이브러리가 수행하며, 절대 수동으로 구축하지 않음 ✓
  • 느린 작업 (Slow operations): 긴 요청의 경우 버퍼 (buffer)를 조기에 플러시 (flush)함 ✓
  • 상태 확인 (Health check): /health 엔드포인트가 200 OK를 반환함 ✓
  • Content-Length: 수동 출력을 사용하는 경우, 명시적인 Content-Length를 설정함 ✓
  • 테스트 (Testing): 최소 두 가지 이상의 서로 다른 MCP 클라이언트로 테스트함 (클라이언트마다 동작이 다릅니다!) ✓
  • 단순함 (Simple): AI가 더 잘할 수 있는 로직을 제거했는가? ✓

2026년에 MCP 서버를 구축할 때의 장단점

솔직하게 말씀드리겠습니다. 저는 MCP의 열렬한 팬이지만, 모든 것이 장밋빛인 것만은 아닙니다.

장점:

  • 표준 프로토콜 (Standard protocol)은 하나의 구현이 어디서나 작동함을 의미합니다. 이는 매우 거대한 변화입니다. 더 이상 모든 AI 클라이언트(AI client)를 위해 맞춤형 통합 기능을 구축할 필요가 없습니다.
  • 프라이버시 측면의 이점: 데이터가 귀하의 서버에 머뭅니다. AI는 현재 쿼리에 필요한 특정 스니펫 (snippets)만 가져갑니다. 모든 노트를 OpenAI나 Anthropic에 업로드할 필요가 없습니다.
  • 단순함이 최고: 아키텍처 (architecture)가 단순함을 유지하도록 강제하며, 이는 거의 항상 좋은 결과로 이어집니다.
  • 생태계가 성장 중: 매달 점점 더 많은 클라이언트가 MCP를 지원하고 있습니다. 무언가 거대한 것의 시작처럼 느껴집니다.
  • 1,800시간 동안 실패했던 저의 사이드 프로젝트에 새로운 생명력을 불어넣어 주었습니다. 그것만으로도 저에게는 충분히 가치가 있습니다.

단점:

  • 생태계가 아직 초기 단계입니다. 클라이언트마다 동작 방식이 다릅니다. 호환되지 않는 부분들을 직접 처리해야 할 것입니다.
  • 디버깅 (Debugging)이 좌절감을 줄 수 있습니다. 무언가 작동하지 않을 때, 그것이 서버의 문제인지 클라이언트의 문제인지 명확하지 않은 경우가 많습니다.
  • 아직 프로덕션 (production) 사례가 많지 않습니다. 현재 나와 있는 것들 중 상당수는 장난감 예제 (toy examples)이며, 이 글과 같은 실전 경험이 담긴 사례는 아닙니다.
  • 서버가 공개적으로 접근 가능해야 합니다. 개발 단계에서는 ngrok이 필요하고, 프로덕션 단계에서는 호스팅 (hosting)이 필요합니다. 추가적인 작업이 발생합니다.

종합: AI 통합을 통해 이점을 얻을 수 있는 프로젝트를 가지고 있다면, 지금 당장 MCP 서버를 구축하는 것은 절대적으로 가치 있는 일입니다. 다만 약간의 거친 부분이 있을 것이라는 점은 예상해야 합니다. 생태계가 여전히 성숙해가는 과정일지라도, 이 패턴 자체는 견고합니다.

다음 단계는?

저는 여전히 MCP에 관한 글을 쓰고 있는데, 그 이유는 저 또한 여전히 배우고 있기 때문입니다. 새로운 MCP 서버를 배포할 때마다, 무언가 새로운 문제가 발생하여 작동이 멈추곤 합니다. 이는 곧 쓸 만한 글이 또 하나 생긴다는 것을 의미합니다.

좋은 소식은 기초적인 부분들이 해결됨에 따라 각 새 글의 길이가 조금씩 짧아지고 있다는 점입니다. 이제는 모든 내용을 반복하는 대신 사람들에게 이 체크리스트를 안내할 수 있습니다.

이제 막 MCP를 시작하신다면, 이 체크리스트가 제가 이 모든 것들을 디버깅하며 보낸 수많은 시간을 여러분에게서 아껴주기를 바랍니다. 기초적인 부분들이 정리되고 나면 MCP 서버를 구축하는 것은 매우 즐거운 일입니다.

대화를 시작해 봅시다

궁금합니다. 혹시 이미 MCP 서버를 구축해 보셨나요? 가장 이상했던 버그는 무엇이었나요? 저처럼 인증 (Authentication) 및 CORS 문제로 고생하셨나요, 아니면 더 수월한 방법을 찾으셨나요?

아래에 댓글을 남겨 알려주세요 — 여러분의 이야기를 듣고 싶습니다. 그리고 이 체크리스트가 도움이 되었다면, MCP 서버를 구축하는 다른 사람들에게 자유롭게 공유해 주세요. 그들이 고마워할 것입니다.

이 기사는 실제 사이드 프로젝트를 위한 MCP 서버 구축에 관한 저의 연재 시리즈 중 일부입니다. 이 체크리스트가 탄생하게 된 실제 MCP 서버를 확인하려면 Papers on GitHub를 방문해 보세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0