본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 25. 23:39

MCP 서버 검증: 89번의 운영 중단 사고를 겪으며 내 MCP 서버에 적절한 요청 검증(Request Validation)을 추가하며 배운 점

요약

LLM의 파라미터 환각 현상으로 인해 발생하는 MCP 서버의 운영 중단 문제를 다룹니다. 요청 검증(Request Validation)의 중요성을 강조하며, 오류를 87% 줄여주는 3계층 검증 아키텍처와 구현 방법을 소개합니다.

핵심 포인트

  • LLM은 도구 스키마를 정확히 읽지 않고 파라미터를 환각할 수 있음
  • 잘못된 파라미터 이름, 타입, 크기로 인해 서버 오류 및 OOM 발생 가능
  • 검증 계층 구축을 통해 MCP 서버의 안정성을 크게 향상 가능
  • Spring Boot 환경에서 Jakarta Bean Validation을 활용한 검증 사례 공유

MCP 서버 검증: 89번의 운영 중단 사고를 겪으며 내 MCP 서버에 적절한 요청 검증(Request Validation)을 추가하며 배운 점

솔직히 말해서, 저는 MCP 운영 이슈는 이제 다 끝났다고 생각했습니다. 89번의 중단 사고를 거치며 연결 문제, 타임아웃, 캐싱, 로깅, 상태 확인(Health checks), 디스커버리(Discovery) 등을 모두 해결했으니까요... 그 외에 무엇이 더 잘못될 수 있겠습니까?

알고 보니, 제가 완전히 과소평가했던 한 가지는 바로 **LLM으로부터 오는 잘못된 입력(Bad input)**이었습니다.

상황을 설명해 보겠습니다: 당신은 온갖 화려한 도구들을 갖춘 멋진 MCP 서버를 구축했습니다. AI 클라이언트가 연결되고, 테스트 단계에서는 모든 것이 완벽하게 작동합니다. 그러다 매일 사용하기 시작하면, 갑자기 이런 상황을 맞닥뜨리게 됩니다:

500 Internal Server Error

그리고 로그를 확인해 보면 이렇습니다:

Caused by: java.lang.IllegalArgumentException: Tool 'search_knowledge_base' doesn't have parameter 'queryy'

또는:

Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.List<java.lang.Double>` from String

아니면 제가 가장 좋아(?)하는 것:

Caused by: java.lang.OutOfMemoryError: Java heap space

누군가(네, 맞습니다, LLM) 10글자짜리 검색 쿼리여야 할 파라미터로 10MB 크기의 문자열을 보냈기 때문입니다.

89번의 운영 중단 사고 중 17번은 요청 검증(Request validation)의 누락이나 부재로 인해 직접적으로 발생했습니다. 이는 거의 20%에 달합니다! 이 모든 것이 LLM이 파라미터 이름, 타입, 크기를 환각(Hallucinate)하기 때문에 발생한 일입니다.

오늘 저는 제 MCP 서버를 위해 적절한 검증 계층(Validation layer)을 구축하며 배운 점, 검증 관련 오류를 87% 줄여준 3계층 아키텍처(Three-layer architecture), 그리고 여러분의 프로젝트에 그대로 복사해서 붙여넣을 수 있는 모든 코드를 공유하고자 합니다.

문제점: LLM은 파라미터를 환각한다. 대처하라.

MCP 서버를 구축해 보셨다면 이미 알고 계실 것입니다: LLM은 당신의 도구 스키마(Tool schema)를 읽지 않습니다. 그들은 추측합니다.

그들은 파라미터 이름을 추측합니다:

  • queryqueryy
  • api_keyapiKey
  • limitmax_results

그들은 파라미터 타입을 추측합니다:

  • number → `

매개변수(parameter) 이름을 수정했다면, 이제 실제 내용을 검증해야 합니다. 저는 Spring Boot를 사용하고 있으므로, @Valid를 이용한 Jakarta Bean Validation을 사용하는 것이 자연스러운 선택입니다.

현재 일반적인 도구 요청 매개변수 클래스는 다음과 같은 모습입니다:

@Data
public class SearchKnowledgeBaseParams {

...

그리고 컨트롤러(controller)에서는 다음과 같이 작성합니다:

@RestController
@RequestMapping("/mcp")
public class McpServerController {
...

잠깐, 여러분은 이렇게 생각할지도 모릅니다. "하지만 MCP 도구들은 동적(dynamic)인데, 모든 도구에 대해 정적 클래스(static class)를 가질 수는 없잖아요!"

맞습니다. 도구가 런타임(runtime)에 추가되는 완전 동적 시스템이라면 이 방식은 작동하지 않을 것입니다. 하지만 (저의 지식 베이스 서버처럼) 고정된 도구 세트를 가진 대부분의 MCP 서버의 경우, 이 방식은 완벽합니다. 단순하고, 표준 라이브러리를 사용하며, 잘 작동하기 때문입니다.

여기서 얻을 수 있는 가장 큰 이점은 크기 제한(size limits) 강제입니다. 이러한 @Size@Max 어노테이션(annotation)을 추가하기 전에는, LLM이 12MB 크기의 쿼리 문자열(query string)을 보내 운영 중단 사고가 발생한 적이 있습니다. 이로 인해 JVM이 OOM(Out Of Memory)으로 충돌했습니다. 이제는 그런 일이 발생하지 않습니다. 즉시 깔끔한 400 에러와 함께 거부됩니다.

레이어 3: LLM과 소통하는 전역 예외 처리기 (Global Exception Handler)

마지막 단계는 LLM이 자신의 실수를 바로잡는 데 실제로 사용할 수 있는, 깔끔하고 이해하기 쉬운 에러 메시지를 반환하는 것입니다.

Spring Boot가 기본으로 제공하는 에러 HTML 페이지를 그대로 반환하게 두면, LLM은 무엇이 잘못되었는지 알 수 없습니다. 그저 계속 추측할 뿐이죠. 대신, 모든 검증 에러를 LLM이 한 번에 읽고 수정할 수 있도록 구조화된 형식(structured format)으로 반환해야 합니다.

저의 예외 처리기(exception handler)는 다음과 같습니다:

@RestControllerAdvice
public class McpExceptionHandler extends ResponseEntityExceptionHandler {

...

이렇게 하면 다음과 같이 깔끔한 JSON 응답이 반환됩니다:

{
  "error": "validation_failed",
  "message": "Your request contains validation errors",
...

LLM은 이를 읽고 무엇이 잘못되었는지 이해하여 한 번의 시도만에 수정할 수 있습니다. 이 방식을 도입하기 전에는 LLM이 계속 틀려서 3~4번씩 대화를 주고받아야 했습니다. 이제는 대부분의 경우 다음 시도에서 바로 수정합니다.

장단점: 이 방식이 당신에게 적합할까요?

솔직히 말씀드리면, 이 방식이 모든 사람에게 맞는 것은 아닙니다. 어떤 점이 효과적이고 어떤 점이 그렇지 않은지 정리해 드립니다.

✅ 장점

  1. 단순하며 표준 라이브러리를 사용함 — 아마 이미 Classpath에 Spring Boot와 Jakarta Validation이 포함되어 있을 것입니다. 추가적인 의존성(Dependency)이 필요하지 않습니다.

  2. 검증 관련 오류의 87%를 포착함 — 이를 배포한 이후 제 운영 통계는 다음과 같습니다:

    • 도입 전: 장애(Outage)의 19%가 검증 관련 문제였습니다.
    • 도입 후: 장애의 2.5%만이 검증 관련 문제입니다. 아주 적은 양의 코드로 얻은 엄청난 개선입니다.
  3. 조기 거부(Early rejection)를 통한 리소스 절약 — 잘못된 요청이 데이터베이스나 서비스 계층(Service layer)에 도달하기 전에 거부됩니다. 불필요한 연결(Connection), CPU 낭비, OOM(Out of Memory) 크래시가 발생하지 않습니다.

  4. LLM 친화적인 에러 메시지 — LLM이 무엇이 잘못되었는지 실제로 이해할 수 있으며, 사람의 개입 없이 스스로 수정할 수 있습니다.

  5. 점진적 도입 가능 — 기존 MCP 서버에 이 방식을 점진적으로 추가할 수 있습니다. 가장 많이 사용되는 도구(Tool)부터 시작하여, 진행하면서 다른 도구들을 추가하면 됩니다.

❌ 단점

  1. 정적(Static) 방식임 — 고정된 도구 세트가 있을 때는 매우 잘 작동합니다. 만약 JSON Schema 검증을 포함하여 런타임에 완전히 동적인 도구 로딩(Dynamic tool loading)이 필요하다면, everit-json-schema와 같이 더 복잡한 도구가 필요할 것입니다.

  2. 여전히 도구당 하나의 클래스를 작성해야 함 — 이는 상용구 코드(Boilerplate)이지만, 솔직히 얻게 되는 타입 안정성(Type safety)과 검증의 가치를 생각하면 그만한 가치가 있다고 생각합니다.

  3. 모든 것을 잡아내지는 못함 — 만약 LLM이 파라미터(Parameter)를 완전히 누락하여 아예 포함하지 않는 경우라면 @NotBlank가 이를 잡아냅니다. 하지만 선택적 파라미터(Optional parameters)가 있는 경우에는 문제가 되지 않습니다.

실제 운영 결과

지난 한 달 동안 제 MCP 지식 베이스(Knowledge base) 서버의 운영 환경에서 이 방식을 실행해 왔습니다. 변화된 내용은 다음과 같습니다:

지표 (Metric)검증 전 (Before Validation)검증 후 (After Validation)변화 (Change)
검증 관련 운영 중단 (Validation-related outages)89건 중 17건91건 중 2건-88%
...

이것은 게임 체인저(game-changing)입니다. 예전에는 어떤 LLM이 매개변수(parameter) 이름을 환각(hallucinate)하여 서버를 다운시켰기 때문에 호출(page)을 받곤 했습니다. 이제 서버는 깔끔하게 400 에러를 반환하고, LLM이 이를 수정하며, 아무도 눈치채지 못합니다.

내가 배운 핵심 교훈 (Key Lessons I Learned)

  1. LLM은 스키마(schema)를 읽지 않습니다 — 이를 인정하고 설계하십시오. LLM은 추측합니다. 별칭(aliasing)을 사용하여 그들이 올바르게 추측할 수 있도록 도와주세요.

  2. 빨리 실패하고, 깔끔하게 실패하십시오 (Fail early, fail cleanly). 어떤 작업을 수행하기 전에 가장자리(edge)에서 검증하십시오. 잘못된 입력이 비즈니스 로직(business logic)에 도달해서는 안 됩니다.

  3. 크기 제한(Size limits)은 타협할 수 없는 사항입니다. 문자열 길이, 배열 크기, 숫자 값 등 모든 것에 항상 최대치를 설정하십시오. 언젠가 LLM이 10MB의 텍스트를 보낼 때, 당신은 이 작업을 해두길 잘했다고 생각할 것입니다.

  4. 모든 에러를 한 번에 반환하십시오. LLM이 여러 차례의 라운드를 거치며 한 번에 하나의 에러만 수정하게 만들지 마십시오. 모든 에러를 한 번에 제공하여 한 번에 모든 것을 수정할 수 있게 하세요. 이것이 훨씬 빠릅니다.

  5. 표준 프로토콜(Standard protocols)은 당신의 친구입니다. MCP는 이미 프로토콜을 정의하고 있습니다. 당신은 그 위에 검증 계층(validation layer)을 추가하기만 하면 됩니다. 모든 호환 가능한 클라이언트는 변경 없이 작동합니다.

전체 그림: 이것이 MCP의 어디에 위치하는가? (The Full Picture: Where Does This Fit In MCP?)

MCP 운영 모범 사례(production best practices)에 관한 90개 이상의 글을 작성한 후, 검증이 전체 스택(stack)에서 어디에 위치하는지 정리하면 다음과 같습니다:

계층 (Layer)해결하는 문제 (What it solves)
연결 유지 (Connection Keep-Alive)LLM이 생각하는 동안 프록시(proxy)가 유휴 연결(idle connections)을 끊는 것을 방지
...

긴 여정이었습니다. 모든 운영 중단 사고는 저에게 새로운 것을 가르쳐 주었으며, 이 검증 계층은 지난 몇 달 동안 제가 수행한 신뢰성(reliability) 개선 작업 중 가장 큰 개선 사항 중 하나였습니다.

다음 단계는? (What's Next?)

이제 일반적인 MCP 운영 이슈 대부분을 다루었습니다:

  • ✓ 연결 관리 (Connection management)
  • ✓ 타임아웃 설정 (Timeout configuration)
  • ✓ 캐싱 (Caching)
  • ✓ 로깅 및 관측성 (Logging & observability)
  • ✓ 상태 확인 (Health checks)
  • ✓ 도구 탐색 (Tool discovery)
  • ✓ 요청 검증 (Request validation)
  • ✓ 인증 (Authentication)
  • ✓ 에러 처리 (Error handling)
  • ✓ CORS 처리 (CORS handling)

남은 것은 무엇일까요? 저는 이 모든 교훈을 한곳에 모은 완전한 "Zero to Production MCP Server" 단계별 튜토리얼을 작성할까 생각 중입니다. 읽어보실 의향이 있으신가요? 댓글로 알려주세요!

여러분의 차례

MCP 서버를 구축해 보셨나요? 어떤 검증 문제에 직면하셨나요? 이 3계층 아키텍처 (three-layer architecture)보다 더 나은 접근 방식을 찾으셨나요? 아래에 댓글을 남겨 여러분이 배운 점을 공유해 주세요. 저도 여전히 배우는 중입니다!

이 내용이 도움이 되었나요? 이 모든 패턴이 포함된 완전한 MCP 지식 베이스 구현을 확인하려면 전체 리포지토리를 확인해 보세요. 유용했다면 Star를 눌러주세요!

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0