본문으로 건너뛰기

© 2026 Molayo

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

MCP 로깅: 프로덕션 MCP 서버에 적절한 로깅을 추가하며 배운 점

요약

프로덕션 환경의 MCP(Model Context Protocol) 서버 운영 시 발생하는 디버깅 문제를 해결하기 위한 로깅 전략을 다룹니다. 요청과 응답 전체를 기록하고, 구조화된 로깅을 통해 효율적인 문제 추적 방법을 제안합니다.

핵심 포인트

  • AI 클라이언트의 예측 불가능한 요청과 JSON 직렬화 오류에 대비해야 함
  • 요청(Request)과 응답(Response)을 모두 로깅하고 실행 시간을 기록할 것
  • 로그 폭증 방지를 위해 대용량 응답은 적절히 절단하여 저장
  • 검색 효율을 높이기 위해 주요 필드에 구조화된 로깅(Structured Logging) 적용
  • 보안을 위해 민감한 정보는 제외하되 그 외 모든 데이터는 기록할 것

MCP 로깅: 프로덕션 MCP 서버에 적절한 로깅을 추가하며 배운 점

솔직히 말해서, 저는 로깅이 백엔드 개발에서

  1. 클라이언트는 AI입니다 — 사람이 보기에는 이상해 보이는 파라미터와 함께, 전혀 예상치 못한 요청을 보낼 수 있습니다.
  2. 사용자는 무슨 일이 일어나고 있는지 이해하지 못합니다 — 그들은 그저 "안 돼요"라고 말할 뿐이며, 상세한 정보를 제공할 방법이 없습니다.
  3. JSON 직렬화 (Serialization)는 깨지기 쉽습니다 — 이스케이프(escape) 처리되지 않은 따옴표 하나, 잘못된 null 값 하나만으로도 전체 응답이 쓰레기가 됩니다.
  4. 디버깅은 사후에 발생합니다 — 무엇이 잘못되었는지 파악하려면 전체 대화 내용이 필요합니다.

제 경우에는, JSON을 수동으로 빌드할 때 제목에 포함된 " 문자를 제대로 이스케이프 처리하지 않았던 것이 버그의 원인이었습니다 (비난하지 마세요 — 에러 핸들링 관련 글에서 아주 힘들게 배운 교훈입니다). 응답을 로깅하지 않았더라면, 저는 영원히 그 버그를 찾아 헤맸을 것입니다.

제가 변경한 내용: 새로운 로깅 접근 방식

그 좌절스러운 디버깅 세션 이후, 저는 MCP 서버의 로깅 방식을 완전히 새로 작성했습니다. 여기 Java/Spring Boot를 예시로 사용하여 제가 최종적으로 구현한 결과물이 있습니다 (하지만 이 원칙은 어떤 언어나 프레임워크에도 적용됩니다).

1. 요청(Request)과 응답(Response) 모두를 로깅하세요 (모든 것을)

당연해 보이지만, 저는 처음에 이 과정을 생략했습니다. 들어오는 요청은 로깅했지만, 나가는 응답은 로깅하지 않았습니다. 그렇게 하지 마세요. 모든 것을 로깅하세요.

현재 제가 사용하는 요청/응답 로깅 필터는 다음과 같습니다:

@Component
public class McpLoggingFilter extends OncePerRequestFilter {

...

이 방식에서 마음에 드는 몇 가지 점은 다음과 같습니다:

  • 필터(Filter) 방식이므로, 어떤 컨트롤러(Controller)가 처리하든 모든 MCP 요청을 잡아냅니다.
  • 실행 시간(duration)을 로깅하므로, 느린 쿼리를 즉시 찾아낼 수 있습니다.
  • 로그 파일이 폭발적으로 커지는 것을 방지하기 위해 큰 응답은 잘라냅니다 (이것 또한 뼈아픈 경험을 통해 배웠습니다 — 전체 지식 베이스 검색 결과는 100KB 이상의 텍스트가 될 수 있습니다).
  • 요청과 응답을 동일한 로그 엔트리에 함께 유지하므로, 두 내용을 매칭하기 위해 수천 줄의 로그를 뒤질 필요가 없습니다.

2. 중요한 필드를 위한 구조화된 로깅 (Structured Logging)

저는 개발 시에는 여전히 일반 텍스트 로깅 (plain text logging)을 사용합니다 (알고 있습니다, 알고 있어요 — 모두가 구조화된 JSON 로깅 (structured JSON logging)이 미래라고 말하지만, 저는 구식입니다). 하지만 나중에 쿼리하고 싶을 수도 있는 중요한 필드들에 대해서는, 사용 중인 로깅 프레임워크가 지원한다면 구조화된 필드 (structured fields)로 추가합니다.

SLF4J에서는 다음과 같은 모습입니다:

log.atInfo()
   .addArgument(requestId)
   .addArgument(method)
...

왜 이렇게 할까요? 하루에 1,000개의 요청이 들어올 때 (좋아요, 저는 하루에 10개 정도 들어옵니다 — 사이드 프로젝트니까요), 실패한 요청 하나를 찾아야 한다면 status=500으로 grep 하는 것이 모든 로그를 스크롤하는 것보다 훨씬 빠르기 때문입니다.

비록 화려한 로깅 인프라를 갖추고 있지 않더라도, 이 습관은 마침내 프로덕션 문제를 디버깅해야 할 때 빛을 발합니다.

3. 비밀 정보는 로깅하지 마세요 — 하지만 그 외의 모든 것은 로깅하세요

말할 필요도 없는 사실이겠지만, 그래도 말씀드리겠습니다: API 키, 인증 토큰(auth tokens), 또는 사용자의 개인정보(PII)를 절대 로깅하지 마세요. 하지만 그 외의 모든 것은 로깅 대상입니다.

제 MCP 서버에서 API 키는 여러 곳을 통해 들어올 수 있습니다 (서로 다른 클라이언트가 각기 다르게 동작하기 때문에 여러 위치를 지원해야 한다고 말했던 인증 관련 기사를 기억하시나요?). 저는 로깅하기 전에 반드시 이를 마스킹(redact)합니다:

private String redactApiKey(String body) {
    // api_key 값을 *** REDACTED ***로 교체
    return body.replaceAll("(?i)(\"api[_-]key\"\s*:\s*")[^"]+", "$1*** REDACTED ***");
...

단순하고 효과적이며, 로그에 비밀 정보가 남지 않도록 유지해 줍니다. 그 외의 모든 것 — 도구 이름(tool names), 파라미터 값(parameter values), 결과 내용(result content) — 은 모두 로깅합니다. 디스크 공간 비용은 로그 없이 디버깅하는 비용보다 훨씬 저렴합니다.

4. MCP 로그를 일반 앱 로그와 분리하세요

이것은 저에게 게임 체인저(game-changer)였습니다. 예전에는 MCP 로그를 다른 모든 애플리케이션 로그와 섞어서 사용했습니다. 이제 저는 로깅 프레임워크를 설정하여 모든 MCP 관련 로그를 별도의 파일로 보내도록 합니다.

logback-spring.xml에서는 다음과 같은 모습입니다:

<appender name="MCP_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/mcp.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
...

이것이 왜 그렇게 훌륭할까요? MCP 문제를 디버깅해야 할 때, 관련 없는 수천 줄의 애플리케이션 로그를 grep으로 뒤지는 대신 단순히 tail -f logs/mcp.log만 실행하면 되기 때문입니다. 이는 시간을 엄청나게 절약해 주는 간단한 변화입니다.

좋았던 점, 나빴던 점, 그리고 예상치 못했던 점: 내가 배운 교훈들

이 새로운 로깅 설정을 몇 주 동안 운영해 본 결과, 저를 놀라게 했던 점들은 다음과 같습니다:

좋았던 점: 아무도 보고하기 전에 버그를 잡아냈다

지난주에 로그를 확인하다가 다음과 같은 내용을 발견했습니다:

Request: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_knowledge","arguments":{"query":"MCP authentication"}}}
Response: {"jsonrpc":"2.0","id":1,"result":{}}

결과값이 비어 있습니다! 왜일까요? 알고 보니 제 검색 쿼리가 지식 베이스(knowledge base)에서 비공개로 표시된 일부 노트와 일치했고, 그 결과 서비스가 빈 리스트를 반환한 것이었습니다. 이것이 엄밀히 말해 버그는 아니지만, 최악의 사용자 경험(user experience)을 제공합니다. AI 클라이언트는 아무것도 받지 못하고 무슨 일이 일어났는지 알 수 없으며, 사용자는 서버가 고장 났다고 생각하게 됩니다.

로그에서 이를 확인한 지 5분 만에 문제를 수정했습니다:

if (results.isEmpty()) {
    return List.of(
            new TextContent("No results found for your query: " + query +
...

이제 AI는 단순히 아무것도 받지 않는 대신, 사용자에게 보여줄 수 있는 사람이 읽을 수 있는 메시지를 받게 됩니다. 응답(response)을 로깅하지 않았다면 저는 절대 이것을 찾아내지 못했을 것입니다. 사용자는 아마 답답함을 느끼고 보고조차 하지 않았을 것이며, 그저 작동하지 않는다고 생각했을 것입니다.

나빴던 점: 로깅은 오버헤드 (Overhead)를 추가할 수 있다 (하지만 대부분의 경우 무시할 만한 수준이다)

전체 요청/응답(request/response) 로깅을 추가한 후 약간의 성능 저하를 감지했습니다. 극적인 수준은 아니었습니다. 요청당 몇 밀리초(ms) 정도의 추가 시간이 발생하는 수준입니다. 하루에 10~20건의 요청이 발생하는 제 사이드 프로젝트에서는 전혀 무의미한 수준입니다.

하지만 트래픽이 많은 MCP 서버를 운영 중이라면 샘플링 (sampling)을 고려해야 할 수도 있습니다. 개발 단계에서는 모든 요청을 기록하고, 프로덕션 (production) 환경에서는 10% 또는 1%만 기록하는 식입니다. 아니면 기본적으로 에러만 기록하고, 디버깅이 필요할 때만 전체 로깅을 활성화할 수도 있습니다.

하지만 저의 경우에는 — 그리고 대부분의 사이드 프로젝트나 심지어 소규모 프로덕션 MCP 서버들도 마찬가지일 것이라 생각합니다만 — 오버헤드 (overhead)를 감수할 가치가 충분합니다. 디버깅 가능성 (debuggability) 측면에서 얻는 이득이 엄청나기 때문입니다.

예상치 못한 발견: 사람들이 실제로 내 MCP 서버를 어떻게 사용하는지 배웠다

이것은 정말 큰 놀라움이었습니다. 저는 사람들이 제 지식 베이스 (knowledge base)를 쿼리할 수 있도록 MCP 서버를 구축했습니다. 저는 사람들이 기술적인 주제를 검색하고 답변을 얻는 것으로 끝날 것이라 예상했습니다.

하지만 로그를 살펴보면서, 사람들이 제가 전혀 예상하지 못한 방식으로 서버를 사용하고 있다는 것을 발견했습니다:

  • 어떤 사람들은 제 노트를 기반으로 기사 전체를 작성해 달라고 요청합니다.
  • 어떤 사람들은 제가 특정 문제를 어떻게 해결했는지 물어봄으로써 자신의 MCP 서버를 디버깅하는 데 도움을 받습니다.
  • 심지어 한 사람은 제가 MCP 서버를 구축한 경험을 바탕으로 면접 질문을 생성하는 데 사용하기도 했습니다.

실제 요청들을 읽어보는 것은 새로운 도구와 개선 사항에 대한 수많은 아이디어를 주었습니다. 저는 현재 사람들이 사용하는 모습을 보고 generate_blog_post 도구를 작업 중입니다. 로깅을 하지 않았다면 이런 생각은 전혀 못 했을 것입니다.

물론 이것은 저의 개인적인 지식 베이스이며, 모든 사용자는 제가 로깅을 하고 있다는 사실을 알고 있습니다 — README에 명시해 두었습니다. 만약 사용자 데이터를 다루고 있다면, 이 부분에 주의해야 하며 투명해야 합니다. 하지만 저의 오픈 소스 사이드 프로젝트의 경우, 사람들이 실제로 어떻게 사용하는지 확인하는 것은 믿을 수 없을 정도로 가치 있는 일이었습니다.

현재 설정의 장단점

솔직히 말씀드리면, 이 설정이 완벽하지는 않습니다. 잘 작동하는 부분과 그렇지 않은 부분은 다음과 같습니다:

장점 ✅

  1. 완전한 가시성 (Full visibility) — 무언가 잘못되었을 때, 한 곳에서 전체 요청(request)과 응답(response)을 확인할 수 있습니다. 더 이상 "실제로 무슨 일이 일어났지?"라며 추측할 필요가 없습니다.
  2. 관심사의 분리 (Separation of concerns) — MCP 로그가 별도의 파일에 저장되므로, 다른 애플리케이션의 노이즈에 섞여 사라지지 않습니다.
  3. 관리가 용이한 절단 (Truncation keeps things manageable) — 대규모 검색 결과로 인해 수 메가바이트(MB)에 달하는 로그 파일이 생성되어 디스크를 가득 채우는 일이 없습니다.
  4. 비밀 정보 마스킹 (Secrets are redacted) — 로그에 API 키가 실수로 유출될 걱정을 하지 않아도 됩니다.
  5. 버그 조기 발견 (Catch bugs early) — 사용자가 보고하기도 전에 세 가지 문제를 발견했습니다. 이는 사용자 경험(UX) 측면에서 매우 큰 이점입니다.

단점 ❌

  1. 여전히 일반 텍스트 (Still plain text) — 모든 로그에 대해 정교한 쿼리를 수행하고 싶다면 수동으로 파싱해야 합니다. 구조화된 JSON 로깅 (Structured JSON logging)이 도움이 되겠지만, 사이드 프로젝트 단계에서는 아직 신경 쓰지 않았습니다.
  2. 요청 상관관계 부재 (No request correlation) — 요청이 여러 서비스를 거치는 경우, 끝까지 추적할 수 있는 요청 ID (request ID)가 없습니다. 다시 말하지만, 단일 서버 사이드 프로젝트에서는 큰 문제가 아니지만, 분산 시스템 (distributed systems)에서는 필요할 것입니다.
  3. 절단이 버그를 숨길 수 있음 (Truncation can hide bugs) — 아주 드물게 버그가 절단된 부분에 포함될 때가 있습니다. 제한 용량을 10KB로 늘려 이 문제를 해결했지만, 여전히 트레이드오프 (tradeoff) 관계에 있습니다.
  4. 개인정보 보호 고려 사항 (Privacy considerations) — 여러 사용자에게 서비스를 제공하는 경우, 사용자 요청을 아예 로깅해야 하는지 고민해야 합니다. 저의 단일 사용자 + 공개 게스트 액세스 설정에서는 괜찮지만, 상황에 따라 다를 수 있습니다.

여러분의 MCP 서버를 위한 권장 사항

제 경험을 바탕으로, 모든 MCP 서버 개발자가 수행하기를 권장하는 사항은 다음과 같습니다:

  1. 요청(Request)과 응답(Response)을 항상 모두 기록하세요 — 이 점은 아무리 강조해도 지나치지 않습니다. 만약 이 글에서 단 한 가지만 얻어 가신다면, 바로 이것이어야 합니다.
  2. 함께 묶어서 관리하세요 — 요청과 응답을 동일한 로그 엔트리(log entry)에 넣으세요. 요청은 시작할 때 기록하고 응답은 끝날 때 다른 곳에 기록하지 마세요. 디버깅(debugging)이 필요할 때 저에게 감사하게 될 것입니다.
  3. 비밀 정보는 마스킹(Redact)하세요 — API 키, 비밀번호 또는 개인정보(PII)를 절대 기록하지 마세요. 그럴 만한 위험을 감수할 가치가 없습니다.
  4. MCP 로그를 별도의 파일에 저장하세요 — 관련 없는 로그들을 뒤질 필요가 없으면 디버깅이 훨씬 쉬워집니다.
  5. 대용량 페이로드(Payload)는 잘라내세요 — 로그에 100KB 분량의 검색 결과가 모두 있을 필요는 없습니다. 99%의 디버깅 사례에서는 10KB면 충분합니다.
  6. 소요 시간(Duration)을 기록하세요 — 사용자가 알아차리기 전에 성능 문제를 파악하는 데 도움이 됩니다.
  7. 필요하다면 샘플링(Sample)하세요 — 트래픽이 높은 경우 모든 것을 기록하지 마세요. 모든 에러는 기록하되, 성공적인 요청은 일정 비율만 샘플링하여 기록하세요.

대상 독자

아주 작은 규모라도 프로덕션(production) 환경을 위한 MCP 서버를 구축하고 있다면, 반드시 이렇게 하세요. 단순히 로컬에서 테스트 중이라 하더라도, 어쨌든 이렇게 하세요. 무언가 고장 났을 때 반드시 필요하게 될 것이며, 프로덕션 서버가 고장 나서 패닉에 빠졌는데 로그조차 없는 상황에서 나중에 추가하는 것보다 처음에 설정해 두는 것이 훨씬 쉽습니다.

솔직히 말씀드리면, 저는 "그냥 단순한 프로토콜인데 얼마나 어렵겠어?"라고 생각하며 적절한 로깅 없이 첫 번째 버전의 MCP 서버를 만들었습니다. 그리고 혹독한 대가를 치르며 배웠습니다. 저처럼 되지 마세요. 음, 사실은—저처럼 되시되, 직접 실수를 저지르는 대신 저의 실수로부터 배우시기 바랍니다.

마치며

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0