MCP 서버 로깅: 1,800시간의 MCP 지식 베이스에 적절한 관측성 (Observability)을 추가하며 배운 점
요약
MCP(Model Context Protocol) 서버 개발 시 관측성(Observability)을 확보하기 위한 구조화된 로깅의 중요성을 다룹니다. 1,800시간의 개발 경험을 바탕으로, 복잡한 계층 구조를 가진 MCP 환경에서 디버깅 효율을 높이는 로깅 전략을 제안합니다.
핵심 포인트
- MCP는 클라이언트-서버-LLM 간의 3계층 구조로 인해 디버깅이 복잡함
- 구조화된 로깅(Structured Logging)은 문제 해결 시간의 80%를 단축함
- 모든 요청(Request)과 응답(Response)을 자동으로 기록하는 필터 구현 권장
- stdout 버퍼링 문제와 같은 인프라 수준의 오류를 식별하기 위해 관측성 필수
MCP 서버 로깅: 1,800시간의 MCP 지식 베이스에 적절한 관측성 (Observability)을 추가하며 배운 점
솔직히 말해서, 처음에는 로깅이 필요하다고 생각하지 않았습니다.
많은 사이드 프로젝트 개발자들처럼, 저는 "내 컴퓨터에서는 잘 돌아가는데, 왜 거창한 로깅이 필요하지?"라고 생각했습니다. 여기저기에 System.out.println()과 e.printStackTrace()를 넣어두는 것만으로도 제게는 충분했습니다. 저는 틀렸습니다. 아주 크게 틀렸습니다.
10개 이상의 MCP 서버를 구축하고 저의 개인 지식 베이스인 Papers에 1,800시간의 개발 시간을 쏟은 끝에, 저는 마침내 적절한 구조화된 로깅 (Structured Logging)을 추가했습니다. 그리고 말씀드리자면, 이것은 첫 주 만에 제가 겪었던 "왜 이게 안 되지?"라는 문제의 80%를 해결해 주었습니다.
만약 당신이 지금 MCP 서버를 구축하고 있으면서 관측성 (Observability)을 건너뛰고 있다면, 이 글은 당신을 위한 것입니다.
MCP 서버에 차별화된 로깅이 필요한 이유
MCP의 핵심은 이것입니다: MCP는 프록시 프로토콜 (Proxy Protocol)입니다. 당신의 서버는 실제로 최종 사용자에게 직접 말을 걸지 않습니다. 서버는 MCP 클라이언트 (Client)와 통신하고, 클라이언트는 LLM과 통신하며, LLM이 사용자에게 전달합니다. 즉, 당신의 서버와 실제 사용자 사이에는 세 개의 계층이 존재합니다.
무언가 잘못되었을 때 — 그리고 정말이지, 무언가는 반드시 잘못될 것입니다 — 당신은 어디를 살펴봐야 할까요?
- 클라이언트가 요청을 보내기라도 했는가?
- 내 서버가 요청을 받았는가?
- 도구 호출 (Tool call)이 올바르게 파싱되었는가?
- 내가 잘못된 형식을 반환했는가?
- LLM이 존재하지 않는 도구 이름을 환각 (Hallucinate)했는가?
적절한 로깅이 있기 전에는, 저는 에디터를 뚫어지게 쳐다보며 추측하고, 모든 곳에 더 많은 println을 집어넣고, 서버를 재시작하며 재현하려고 노력했습니다. 그것은 매우 진을 빼는 일입니다. 지난달에는 Claude Desktop이 왜 제 서버에 연결되지 못하는지 디버깅하는 데 3시간을 썼습니다. 알고 보니 HTTP 서버를 시작한 후에 stdout으로 로깅을 하고 있었고, 버퍼링 (Buffering)이 연결을 끊어버리고 있었습니다. 3시간이나 걸렸습니다. 고작 로깅 문제 하나 때문에 말이죠.
그때 저는 마침내 처음부터 했어야 했던 일을 했습니다: 바로 적절한 구조화된 로깅 (Structured Logging)을 추가하는 것이었습니다.
내가 만든 것: MCP를 위한 구조화된 로깅 (Structured Logging)
저는 MCP 서버를 위해 Java Spring Boot를 사용하고 있으므로, Spring Boot에서 기본적으로 제공하는 SLF4J/Logback을 선택했습니다. 하지만 여기서 설명하는 원칙은 어떤 언어나 프레임워크에도 적용됩니다.
처음에 제가 놓쳤던 핵심 통찰은 다음과 같습니다: MCP는 잘 정의된 엔드포인트(endpoints)를 가지고 있으므로, 모든 요청(request)과 응답(response)을 자동으로 로깅해야 합니다.
제가 만든 로깅 필터(logging filter)는 다음과 같습니다:
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
...
그게 전부입니다. 단 40줄의 코드로, 갑자기 들어오는 모든 요청을 볼 수 있게 되었습니다. Claude Desktop이 "연결 실패(connection failed)"라고 말할 때, 요청이 제 서버에 도달했는지 확인할 수 있습니다. 만약 도달하지 않았다면 문제는 제 코드가 아니라 Claude의 설정에 있는 것입니다. 만약 도달했다면, 제가 어떤 상태 코드(status code)를 반환했는지 확인할 수 있습니다.
하지만 여기서 끝이 아닙니다. MCP의 핵심은 도구 호출(tool calls)입니다. 그래서 가장 중요한 두 가지 엔드포인트인 tools/list와 tools/call에 대해 구체적인 로깅을 추가했습니다.
도구 호출 로깅: LLM은 항상 실수하기 때문입니다
LLM 및 MCP와 작업할 때의 숨겨진 비밀이 하나 있습니다: LLM은 잘못된 도구 이름을 호출할 것입니다. 파라미터(parameters)를 망가뜨릴 것입니다. 당신이 전혀 예상하지 못한 행동을 할 것입니다. 그리고 로깅이 없다면, 왜 그런 일이 발생하는지 절대 알아낼 수 없을 것입니다.
저는 MCPServerController에 다음을 추가했습니다:
@RestController
@RequestMapping("/mcp")
public class McpServerController {
...
왜 전체 파라미터나 전체 응답을 로깅하지 않을까요? 두 가지 이유가 있습니다:
- 개인정보 보호 (Privacy): 제 지식 베이스(knowledge base)에는 개인적인 노트가 포함되어 있습니다. 로그에 전체 내용이 남는 것을 원치 않습니다.
- 크기 (Size): 도구 응답은 킬로바이트(KB)에서 심지어 메가바이트(MB) 단위까지 매우 클 수 있습니다. 로그 파일이 하룻밤 사이에 비대해질 것입니다.
메타데이터(metadata)만 로깅하세요: 어떤 도구가 호출되었는지, 응답 크기가 얼마였는지, 성공했는지 여부 등입니다. 이것이 디버깅(debugging)에 필요한 사항의 90%입니다.
또 다른 중요한 엔드포인트는 tools/list입니다. 모든 클라이언트는 시작 시 이 엔드포인트를 호출합니다. 이것이 실패하면 아무것도 작동하지 않습니다.
@PostMapping("/list")
public ResponseEntity<McpListToolsResponse> listTools() {
log.info("MCP_TOOLS_LIST_REQUEST");
...
이것이 저를 얼마나 많이 구했는지 말로 다 표현할 수 없습니다. 한 번은 도구(tool) 중 하나의 설명(description)이 null이라서 도구 목록의 JSON 직렬화(serialization)가 실패하는 문제가 있었습니다. LLM 클라이언트는 아무런 에러 메시지도 없이 그냥 연결을 종료해 버렸죠. 하지만 로그를 확인하니 tools/list에서 JSON 매핑 예외(JSON mapping exception)가 발생하며 실패했다는 것을 알 수 있었습니다. 2시간이 걸릴 디버깅을 2분 만에 끝낸 셈입니다.
인증 로깅: 모두가 API 키를 엉뚱한 곳에 넣기 때문에
제가 이전에 작성했던 MCP 인증 (MCP authentication)에 관한 글을 기억하시나요? 저는 서로 다른 클라이언트들이 각기 다른 방식을 기대하기 때문에 API 키를 네 가지 다른 위치에서 지원합니다.
그런데 결과가 어땠을까요? 사람들은 여전히 키를 잘못된 위치에 넣습니다. 로깅을 통해 저는 정확히 어떤 일이 일어났는지 확인할 수 있습니다:
@Component
public class McpAuthFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(McpAuthFilter.class);
...
절대로 전체 API 키를 로깅하지 마세요. 그것이 로그로 비밀 정보(secrets)가 유출되는 방식입니다. 어떤 키인지 식별할 수 있을 정도로 앞의 4글자만 로깅하세요. 그것만으로도 충분합니다.
지난주에 누군가 제 서버에 연결하려고 시도하는데 계속 401 에러가 발생하는 문제가 있었습니다. 그들은 분명히 키를 올바른 위치에 넣었다고 확신했죠. 로그를 확인해보니 "MCP_AUTH_MISSING"이라고 떠 있었습니다. 즉, 키가 전혀 전달되지 않고 있었던 것입니다. 알고 보니 그들은 헤더 이름(header name)을 잘못 지정하고 있었습니다. 5분 만에 해결했습니다. 로깅이 없었다면? 우리는 여전히 서로 추측하며 대화를 주고받고 있었을 것입니다.
제 주말을 구해준 CORS 로깅
만약 MCP 서버를 인터넷에 노출한다면 (저는 여러 클라이언트에서 사용하기 위해 그렇게 합니다), CORS가 필요합니다. 그리고 웹 개발을 해보셨다면, CORS가 끊임없이 문제를 일으키는 요소라는 것을 알고 계실 겁니다.
MCP와 CORS 사이에서 가장 최악의 문제는 무엇일까요? 프리플라이트 (preflight) OPTIONS 요청에는 인증 정보가 포함되지 않으며, 만약 이 요청을 거부하면 전체 연결이 아무런 메시지 없이 실패한다는 점입니다.
이 내용은 제 에러 핸들링 (error handling) 관련 글에서도 다루었지만, 핵심은 이것입니다: CORS 프리플라이트 (preflight) 체크를 로그로 남기세요.
@Configuration
public class CorsConfig implements WebMvcConfigurer {
private static final Logger log = LoggerFactory.getLogger(CorsConfig.class);
...
그게 전부입니다. 그리고 제 인증 필터 (auth filter)에서는 OPTIONS 요청에 대해 명시적으로 인증을 건너뜁니다:
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
log.debug("CORS_PREFLIGHT: skipping auth for OPTIONS");
filterChain.doFilter(request, response);
...
이 로그 라인이 없었다면, 프리플라이트가 왜 실패하는지 알아내기 위해 몇 시간 동안 헤맸을 것입니다. 하지만 이 로그 덕분에 인증을 건너뛰고 있으며, 모든 것이 예상대로 작동하고 있다는 것을 즉시 확인할 수 있습니다.
현재 설정의 장단점
솔직해집시다. 완벽한 것은 없습니다. 잘 작동하는 부분, 그렇지 않은 부분, 그리고 만약 처음부터 다시 시작한다면 다르게 할 점들을 정리했습니다.
잘 작동하는 점 (Pros)
✅ 낮은 오버헤드 (Low overhead): 이것은 단지 필터와 몇 개의 로그 문구일 뿐입니다. 추가적인 서비스나 비용이 많이 드는 작업이 없습니다. 운영 환경 (production)에서도 성능 영향은 무시할 수 있는 수준입니다. 저는 월 5달러짜리 VPS를 사용 중인데도 전혀 체감되지 않습니다.
✅ 설계에 의한 프라이버시 (Privacy by design): 개인적인 콘텐츠는 로그로 남기지 않고 메타데이터 (metadata)만 남깁니다. 사용자의 노트는 비공개로 유지되면서도, 저는 디버깅 (debugging)에 필요한 모든 정보를 얻을 수 있습니다.
✅ 모든 환경과 호환: SLF4J는 모든 로깅 프레임워크 (logging framework)와 작동합니다. Logback, Log4j2 등 무엇이든 사용할 수 있습니다. 로그를 파일, Elasticsearch, Datadog 등으로 보내든 상관없습니다.
✅ 문제의 90%를 조기에 포착: 대부분의 MCP 문제는 연결 문제, 잘못된 도구 호출 (tool calls), 인증 문제 등입니다. 이 방식은 이 모든 것을 즉시 잡아냅니다.
✅ 점진적인 추가 용이성: 애플리케이션 전체를 다시 작성할 필요가 없습니다. MCP 엔드포인트 (endpoints)에 필터를 추가하기만 하면 80%는 완료된 것입니다.
잘 작동하지 않는 점 (Cons)
❌ 요청 본문 (request body) 로깅 불가: 전체 요청 본문을 로그로 남기지 않기 때문에, 때때로 기이한 파싱 (parsing) 문제를 놓칠 때가 있습니다. 다시 할 수 있다면, 로컬 디버깅 시에만 전체 본문을 로깅할 수 있는 옵션을 추가하되, 운영 환경에서는 절대 사용하지 않도록 설정할 것입니다.
❌ 요청 간 상관관계 없음 (No correlation across requests): 아직 요청 ID (request ID)가 없습니다. 만약 동시 요청이 발생한다면, 서로 다른 요청의 로그가 뒤섞일 수 있습니다. 간단한 해결책은 MDC에 요청 ID를 추가하는 것이지만, 아직 구현하지 않았습니다.
❌ 집계 불가 (No aggregation): 현재는 단순히 파일에 로그를 남기고 있습니다. 트래픽이 더 많아진다면, 도구 이름(tool name), 상태 코드(status code) 등으로 쿼리할 수 있도록 ELK나 Grafana Loki 같은 도구에 이 로그들을 집계하고 싶을 것입니다. 개인 프로젝트 수준에서는 파일로도 충분하지만, 대규모 운영 환경(production at scale)에서는 더 많은 기능이 필요할 것입니다.
❌ 무엇을 로깅할지 여전히 학습 중: 무엇이 유용한지 여전히 발견해 나가는 과정에 있습니다. 예를 들어, 최근에는 LLM이 존재하지 않는 도구를 요청할 때 로깅하는 기능을 추가했는데, 이는 LLM이 무엇을 환각 (hallucination)하고 있는지 확인하는 데 매우 도움이 되었습니다.
if (!toolExists(request.getName())) {
log.warn("MCP_TOOL_NOT_FOUND: requestedTool={}, availableTools={}",
request.getName(), availableToolNames);
...
이 한 줄의 코드를 통해, LLM이 실제 정의된 이름은 아니지만 그럴듯하게 들리는 도구 이름을 환각하는 경향이 매우 강하다는 것을 이미 배웠습니다.
비싼 대가를 치르고 배운 뼈아픈 교훈
솔직히 말씀드리면, 제대로 된 로깅을 추가하는 것이 개인 프로젝트에는 과하다고 느껴져서 피해 왔습니다. "나 혼자 쓰는 건데,"라고 생각했죠. "왜 이런 엔터프라이즈급 기능들이 다 필요할까?"라고 말입니다.
하지만 저는 개인 프로젝트조차도 훌륭한 관측성 (observability)이 필요하다는 것을 고생하며 배웠습니다. 그 이유는 다음과 같습니다:
- 6개월 뒤에는 본인이 짠 코드의 동작 방식을 잊어버리게 됩니다: 제가 보장합니다. 6개월 뒤에 무언가 고장 났을 때, 당신은 로그 덕분에 살았다며 저에게 감사하게 될 것입니다.
- 결국 다른 사람들도 당신의 서버를 사용하게 됩니다: 설령 친구 한 명뿐이라 할지라도, 그들에게 문제가 생기면 당신은 디버깅을 해야 합니다.
- 클라이언트마다 동작이 다릅니다: Claude Desktop에서는 잘 작동하는 것이 당신의 커스텀 클라이언트에서는 작동하지 않을 수도 있고, Cursor에서 안 될 수도 있으며, 다음 달에 출시될 새로운 MCP 클라이언트에서도 작동하지 않을 수 있습니다.
적절한 로깅을 추가하는 데 들인 시간보다 로그 없이 디버깅하는 데 더 많은 시간을 보냈습니다. 이것이 아이러니입니다. 구조화된 로깅 (Structured Logging)을 추가하는 데 투자한 4시간이 이미 40시간 이상의 디버깅 시간을 아껴주었습니다.
여러분의 실행 항목: 오늘 무엇을 추가해야 하는가
모든 것을 한꺼번에 해결하려고 할 필요는 없습니다. 만약 지금 MCP 서버를 구축하고 있다면, 오늘 당장 다음 세 가지만 추가하세요:
- 요청/응답 필터 (Request/Response filter): 모든 MCP 요청의 메서드 (Method), 경로 (Path), 소요 시간 (Duration), 상태 (Status)를 기록합니다.
- tools/list 및 tools/call에 대한 특정 로깅: 도구 (Tool) 이름과 성공 여부를 기록합니다.
- 인증 로깅 (Authentication logging): API 키가 누락되었거나 유효하지 않을 때를 기록하되, 전체 키를 절대 기록하지 마세요.
그게 전부입니다. 코드 100줄 정도면 됩니다. 한 시간 정도의 작업입니다. 그리고 이것은 나중에 여러분의 고통을 엄청나게 줄여줄 것입니다.
전체 구현 내용을 보고 싶다면 GitHub의 제 Papers 저장소를 확인하세요. 모두 오픈 소스이므로 코드를 살펴보고 필요한 것을 복사해 가셔도 됩니다.
여러분의 경험은 어떠신가요?
저는 몇 달 동안 MCP 서버를 구축해 오고 있으며, 이 새로운 패턴에 적합한 좋은 관측성 (Observability)이 무엇인지 여전히 배우는 중입니다. MCP는 일반적인 REST API와는 다릅니다. 흐름이 다르고, 장애 모드 (Failure modes)가 다르며, 디버깅 요구 사항도 다릅니다.
- MCP 서버를 구축하고 계신가요? 로깅을 위해 무엇을 하고 계신가요?
- 로깅이 있었다면 잡아낼 수 있었을, MCP와 관련하여 겪었던 가장 이상한 디버깅 문제는 무엇이었나요?
- 제가 명백한 무언가를 놓치고 있나요? 더 많이 로깅해야 할까요? 아니면 더 적게? 혹은 완전히 다른 무언가여야 할까요?
아래에 댓글을 남겨 알려주세요. 저는 항상 다른 사람들이 무엇을 하는지로부터 배우고자 합니다. 그리고 이 글이 도움이 되었다면 저장소에 스타 (Star)를 눌러주세요. 다른 사람들이 이 글을 찾는 데 도움이 됩니다.
즐거운 빌딩 되시길 — 그리고 즐거운 로깅 되시길 바랍니다!
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기