
MCP 서버의 품질은 유닛 테스트로 지킬 수 없다 | 779번의 필드 트라이얼로 마찰을 사냥하는 방법론
요약
MCP 서버 개발 시 유닛 테스트만으로는 포착할 수 없는 '첫 통합 경험'의 품질 문제를 다룹니다. 필드 트라이얼(Field Trial) 방법론을 통해 실제 사용 환경에서의 마찰을 식별하고 해결하는 구체적인 프로토콜을 제시합니다.
핵심 포인트
- 유닛 테스트는 코드 내부 로직은 검증하지만 통합 접점의 문제는 놓침
- 품질의 핵심은 인간이나 AI 에이전트가 환경을 올바르게 구성할 수 있는가에 있음
- 필드 트라이얼(FT)을 통해 외부 환경에서의 실제 사용 경험을 계측해야 함
- 마찰 ID와 직렬 게이트를 활용한 체계적인 품질 관리 프로토콜 제안
서론
작은 MCP 서버를 OSS로 공개했는데, 유닛 테스트(Unit Test)는 통과(Green)함에도 불구하고 사용자가 첫 통합 단계에서 막히는 상황, 그런 품질 문제로 고민하고 계신 분들께.
작은 MCP 서버를 OSS로 공개하다 보면 어떤 불편한 사실에 맞닥뜨리게 됩니다. 유닛 테스트가 전부 통과하더라도, 사용자는 막힙니다.
제가 유지보수하고 있는 nene-mcp는 Composer에서 들어오는 MCP 브릿지입니다. 하는 일은 단순합니다. MCP 호스트(Cursor / Claude Desktop 등)로부터의 JSON-RPC를 로컬의 HTTP API로 stdio를 통해 전달하는 것뿐입니다. 로직의 대부분은 순수 함수(Pure Function)로 되어 있어 테스트를 작성할 수도 있습니다.
그럼에도 불구하고, 가치가 결정되는 지점은 테스트 밖에 있었습니다. composer require를 하고, tools.json을 연결하고, MCP 호스트를 설정한 뒤에야 비로소 API를 호출하는——그 "첫 통합 경험(First Integration Experience)"이야말로 제품의 품질 그 자체인데, 그 부분은 유닛 테스트로는 전혀 관측할 수 없습니다.
이 기사는 그 관측할 수 없는 품질을 어떻게 지킬 것인가에 대해, nene-mcp에서 채택한 **필드 트라이얼(Field Trial, 이하 FT)**이라는 방법론에 관한 이야기입니다. "테스트를 늘리자"가 아니라, "테스트로는 보이지 않는 것을 다른 계측 도구(Instrument)로 어떻게 포착할 것인가"라는 관점에서 작성합니다.
이 기사에서 알 수 있는 것:
- 유닛 테스트의 사정거리 밖에 있는 "첫 통합 경험"이라는 품질 계층의 정체
- 마찰 ID(Friction ID)·직렬 게이트(Serial Gate)
- 보안 리뷰에서 마찰을 반드시 정산하는 운영 프로토콜 - "마찰 제로는 패배"——난이도 램프(Difficulty Ramp)를 통해 실제 통합자가 막힐 때까지 스코프를 높이는 사고방식
왜 유닛 테스트로 지킬 수 없는가
MCP 서버의 결함은 코드 내부가 아니라 **통합의 접점(Integration Seams)**에서 발생합니다. nene-mcp의 설계 판단을 기록한 ADR 0001은 "내부 유닛 테스트가 관측할 수 없는 것"을 명시적으로 나열하고 있습니다.
- Cursor / Claude Desktop 등 MCP 클라이언트 설정의 마찰
- 절대 경로를 요구하는 환경 변수 관련 문제
- 카탈로그(
tools.json)와 OpenAPI 정의 사이의 불일치 - 쓰기 계열 도구를 위한 Bearer 토큰 설정
- 보안 사고(MCP를 운영 환경에 적용하려 할 때,
nene_mcp_about이 토큰을 유출하는 등)
이것들은 모두 "코드가 올바른가"의 문제가 아니라, "인간이나 AI 에이전트가 새로운 환경에서 올바르게 구성할 수 있는가"의 문제입니다. 단체 테스트는 자신이 작성한 전제 조건 안에서만 작동하기 때문에, 전제 자체가 틀린 경우——문서가 실제 동작과 어긋나 있거나, 설정 절차에 암묵적인 지식이 필요한 경우——를 구조적으로 놓치게 됩니다.
그래서 발상을 반대로 합니다. 품질을 "내부에서 증명하는" 것이 아니라, "외부에서 실제로 사용하며 망가뜨려 보는 것". 그것이 FT입니다.
마찰 ID와 직렬 게이트
FT의 운영은 상상 이상으로 금욕적인 프로토콜에 의해 통제됩니다. 핵심은 두 가지, **마찰 ID(Friction ID)**와 **직렬 게이트(Serial Gate)**입니다.
마찰 ID (F-N)
각 트라이얼은 저장소 외부의 작업 디렉토리에서 "작지만 현실적인 통합"을 처음부터 구축합니다. vendor/를 재사용하지 않고, 이미 세팅된 환경을 다시 쓰지 않으며, 공개된 문서에만 의존하여 소스를 읽지 않는——철저하게 "신규 사용자의 첫 경험"을 재현합니다.
그 과정에서 막힌 점, 놀란 점, 미세하게 불편했던 점(Papercut)을 모두 F-1, F-2, ... 라는 안정적인 마찰 ID로 기록합니다. 이 ID는 단순한 메모가 아니라, 후속 Issue, 수정, 회귀 테스트(Regression Test)까지 추적할 수 있는 뼈대가 됩니다. 마찰은 다음 세 가지 중 하나로 분류되며, 수정 대상이 명시됩니다.
- fix-in-package:
nene-mcp본체를 수정 - document: 문서로 보완
- fix-in-host: 호스트 측 리포지토리(NeNe 또는 앱 측)에서 수정
fix-in-host는 "브릿지 외부"의 문제를 브릿지의 문서로 흡수시키지 않기 위한 분류입니다. 트라이얼은 패키지 경계를 넘나들 수 있지만, 수정은 올바른 리포지토리에 두어야 한다는 규율이 여기에 나타나 있습니다.
"FT{N+1}은 FT{N}의 issue를 닫을 때까지 시작하지 않는다"
그리고 가장 효과적인 것이 직렬 게이트(Serial Gate)입니다. ADR과 품질 전략(Quality Strategy) 문서 모두에 다음 문장이 명시되어 있습니다.
FT{N+1} does not start until FT{N} Issues are closed.
(FT{N+1}은 FT{N}에서 발생한 Issue가 닫힐 때까지 시작하지 않는다)
품질 전략 문서는 이를 더욱 엄격하게 정의하여, "머지(Merge)된 수정 사항, 검증된 해결책, 또는 follow-ups.md에 명시적인 defer(연기) 행 중 하나"로 모든 Issue가 닫히지 않는 한 다음 FT로 진행할 수 없습니다. 게다가 defer는 예외 사항으로 취급되며(기본값으로 설정하지 않음), nene-mcp 리포지토리 측에서는 "해당 FT에 연결된 open Issue가 0개"인 상태를 착수 조건으로 삼습니다.
이는 언뜻 보면 단순한 운영 규칙처럼 보이지만, 그 효과는 본질적입니다. 마찰(Friction)을 발견하고 방치하는 것이 불가능해집니다. FT를 반복할수록 미수정된 기술 부채가 쌓이는 병렬 운영 방식과 달리, 직렬 게이트는 "발견한 마찰은 다음 단계로 넘어가기 전에 반드시 정산한다"는 것을 구조적으로 강제합니다. 트라이얼 횟수가 곧 "해결된 마찰의 수"가 됩니다.
부작용으로는 트라이얼 하나가 끝난 뒤에 Issue와 PR이 한꺼번에 쏟아져 나온다는 점이 있습니다. ADR은 "1회의 FT 이후 3~7개의 PR이 나오는 것은 예상 범위 내"라고 명시하고 있으며, 이는 이상 현상이 아니라 설계된 대로의 동작입니다.
N % 3 == 0은 보안 리뷰
마찰 ID와 직렬 게이트에 더해, 주기적인 보안 리뷰가 포함되어 있습니다. 규칙은 간단합니다. FT 번호 N이 3의 배수(N % 3 == 0)일 때, 해당 트라이얼에서는 전체 보안 리뷰(Full Security Review)를 필수적으로 수행한다는 것입니다.
이는 즉흥적인 아이디어가 아니라, 자매 프로젝트에서 유래한 케이던스(Cadence)입니다. ADR 0001은 nene2-python이 190개 이상의 트라이얼을 "3의 배수마다 보안 리뷰를 수행"하는 동일한 주기로 진행했던 것을 참조 원천으로 꼽고 있습니다. nene-mcp는 이를 패키지 규모로 계승했습니다.
실제 리포트를 보면 이 케이던스는 제대로 작동하고 있습니다. 예를 들어 FT777(777은 3의 배수)의 리포트에는 전용 "Security Review (required when N % 3 == 0)" 섹션이 있으며, 쓰기 권한의 fail-closed(Bearer가 없으면 작성 불가)를 재검사한 기록이 남아 있습니다. 3의 배수가 아닌 FT778의 리포트에는 동일한 칸에 "N/A — security review scheduled for FT780"이라고만 적혀 있어, 다음 보안 회차가 예약되어 있음을 보여줍니다.
"매번 전체 보안 리뷰를 수행하는 것"은 이상적이지만 현실에서는 형식적으로 흐르기 쉽습니다. 3번에 한 번으로 압축함으로써, 반드시 수행되면서도 너무 무겁지 않은 지속 가능한 밀도를 유지하는 것이 핵심입니다. 쓰기 도구, Bearer, 새로운 HTTP 서피스(Surface)와 같이 보안 영향이 큰 변경 사항은 이 주기에 반드시 한 번은 정면으로 점검됩니다.
FT의 규모와 운영 방식
그렇다면 이 금욕적인 프로토콜을 실제로 얼마나 수행했을까요?
집필 시점을 기준으로 nene-mcp의 필드 트라이얼 리포트는 FT779까지 도달했습니다(docs/field-trials/에 개별 리포트 779개 존재). 최근의 FT778이 "다음은 FT780에서 보안 리뷰"라고 예약한 대로, 780번째(3의 배수이므로 보안 회차)가 다음에 대기 중인 상황입니다. 패키지 규모의 작은 MCP 브릿지 하나를 대상으로 하기에는 상당한 반복량입니다.
다만, 숫자의 내용에는 주의가 필요합니다. 품질 전략 문서는 "FT의 개수를 세는 것 자체가 목적은 아니다"라고 반복해서 강조하고 있습니다. 중간에 포함된 일괄 로그(2026-05의 FT19~FT100 배치)는 "자동 회귀 실행(ft-cycle.sh)을 나중에 라벨링한 것"이며, 개별 리포트를 가진 본래의 FT와는 별개라고 명시되어 있습니다. 자동 회귀는 FT를 보완하는 것이지 대체하는 것이 아닙니다. 따라서 '779'는 양질의 데이터가 섞인 총수이며, 모든 것이 수동으로 진행된 풀(Full) FT인 것은 아닙니다. 이 부분은 솔직하게 구분해 둡니다.
운영 방식의 흥미로운 점은 페르소나 난이도 램프(Persona Difficulty Ramp, FT201 이후)입니다. 트라이얼은 "자동 테스트가 통과할 때까지"가 아니라 "실제 통합자가 막힐 때까지" 스코프를 계속해서 높여갑니다.
| Tier | 예시 | 업무/인증 스코프 | 예상되는 마찰(Friction) |
|---|---|---|---|
| L1 | FT201 | 헬스 체크 + 가짜 read 1회 (404) | 설치의 pin, 부트스트랩 링크, 카탈로그 예시 |
| ... | |||
| 규칙도 철저해서, 'L1 체크만 돌렸는데 마찰이 없다'고 보고하면 안 된다(무엇을 아직 시도하지 않았는지를 적어라)', '실행 가능한 발견(actionableな発見)이 2개 미만이면 다음 단계로 넘어가기 전에 tier를 올리라(툴/인증/배포를 늘려라)'와 같이 정하고 있습니다. 즉, 너무 쉬워서 마찰이 없다면, 그것은 합격이 아니라 난이도 부족의 신호로 간주합니다. |
배운 점
779번을 돌려보면서 알게 된 것은 다음입니다.
MCP 서버는 '사용되는 방식'으로만 고장난다. 로직 버그보다 먼저, 통합의 이음매(설정/경로/토큰/문서의 불일치)에서 사람이 막힌다. 그곳은 유닛 테스트의 사정거리 밖이므로, 실제 활용의 반복 자체를 회귀 스위트(regression suite)로 만들 수밖에 없다. FT는 그 '반복을 시스템화한 것'입니다.
마찰을 찾는 것이 성공이다. 품질 전략 문서와 FT의 README에는 명확하게 이렇게 쓰여 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기