24. 테스트 전략과 실제 사례 패턴
🎯 이 장의 목표
- 단위·통합·보안을 아우르는 테스트 전략을 세운다
- 자주 쓰는 서버 패턴(DB·API·파일·브라우저 래핑)의 설계 포인트를 안다
- 안티패턴을 피하고, 좋은 서버의 특징을 종합한다
본문의 마지막 장입니다. 지금까지의 모든 원칙을 테스트와 실전 패턴으로 묶습니다.
테스트 전략 — 4축 + 보안
16장의 인메모리 테스트를 전략으로 확장합니다. 견고한 서버는 최소한 다음을 덮습니다.
flowchart TD
T["테스트 전략"] --> F["① 기능\n유효 입력 → 올바른 결과"]
T --> V["② 검증\n잘못된 입력이 걸러지나"]
T --> E["③ 에러\n실행 실패가 isError로 오나"]
T --> S["④ 스키마\noutputSchema↔structuredContent 일치"]
T --> SEC["⑤ 보안\n경로탈출·주입·권한 우회 시도"]
classDef t fill:#bfdbfe,stroke:#1d4ed8,color:#000;
classDef a fill:#5eead4,stroke:#0f766e,color:#000;
classDef sec fill:#fca5a5,stroke:#b91c1c,color:#000;
class T t;
class F,V,E,S a;
class SEC sec;
- 기능: 유효 입력에 올바른 결과를 내는가.
- 검증: 타입·범위 위반 입력이 본문 전에 걸러지는가(15장).
- 에러: 비즈니스 실패가 예외가 아니라
isError로 오는가(16장). - 스키마:
outputSchema를 선언했으면structuredContent가 맞는가(10장). - 보안: 경로 탈출(
../../etc/passwd), 주입 문자열, 권한 밖 자원 접근 시도가 거부되는가(21장).
PYTHON
# test_security.py — 경로 탈출이 막히는지 import pytest from mcp.shared.memory import create_connected_server_and_client_session as connect from server import mcp @pytest.mark.anyio async def test_path_traversal_blocked(): async with connect(mcp._mcp_server) as client: result = await client.call_tool("read_file", {"name": "../../etc/passwd"}) assert result.isError # 경계 밖 접근은 거부되어야 @pytest.mark.anyio async def test_valid_read_ok(): async with connect(mcp._mcp_server) as client: result = await client.call_tool("read_file", {"name": "notes.txt"}) assert not result.isError
💡 팁
인메모리는 빠르고, Inspector는 손으로: 자동화 회귀는 인메모리 단위 테스트로(16장), 탐색적 확인은 Inspector로(14장). 원격 서버는 배포 후 헬스·통합 테스트(22장·23장)도 더합니다.실제 사례 패턴
MCP 서버의 공통 패턴은 "기존 API·데이터 소스를 수정 없이 AI 접근 가능하게 만드는 중간 번역기"입니다. 데이터베이스도 CI/CD도 MCP를 알 필요가 없고, 서버가 그 사이에서 번역합니다.
1) 데이터베이스 래핑
flowchart LR
M["모델"] -->|"tool: run_query"| S["MCP Server"]
S -->|"읽기 전용 커넥션"| DB[("DB")]
S -.->|"resource: schema"| M
classDef m fill:#ddd6fe,stroke:#6d28d9,color:#000;
classDef s fill:#5eead4,stroke:#0f766e,color:#000;
classDef d fill:#fde68a,stroke:#b45309,color:#000;
class M m;
class S s;
class DB d;
- 스키마는 Resource로, 질의는 Tool로: 스키마/메타데이터는 읽기 전용 컨텍스트라 리소스가 자연스럽고(11장), 질의 실행은 모델이 부르는 행동이라 도구입니다(10장). 단 모델이 스키마를 자동으로 봐야 하면 도구로도 노출(18장).
- 🔒 읽기 전용 커넥션·파라미터 바인딩: SQL 주입을 막으려 사용자 입력을 쿼리에 직접 잇지 말고 파라미터 바인딩을 쓰며, 가능한 한 읽기 전용 권한으로(21장). 파괴적 질의는 스코프 + 사용자 확인(20장).
2) REST/외부 API 래핑
- 외부 API 엔드포인트를 도구로 노출합니다. OpenAPI 스펙에서 도구를 자동 생성하는 접근도 있습니다(standalone
fastmcp의from_openapi등 — 버전 확인). OpenAPI는 REST API의 엔드포인트·인자·응답을 기계가 읽을 수 있게 기술하는 표준 명세 형식으로, 이 명세가 있으면 도구 정의를 자동으로 뽑아낼 수 있습니다. - 🔒 업스트림엔 별도 토큰: 클라이언트 토큰을 패스스루하지 말 것(19장, confused deputy). 외부 요청 목적지를 허용목록화해 SSRF 차단(21장).
- 큰 응답은 요약·페이지네이션으로 컨텍스트 비용 절감(18장).
3) 파일시스템 래핑
4) 브라우저·자동화 래핑
서버는 얇게, 대체로 무상태
반복되는 설계 원칙: MCP 서버는 내부 시스템 위의 인터페이스 계층이 되는 게 좋습니다(18장). 자체적으로 두꺼운 상태 계층이 되기보다 얇게 유지하면, stateless 확장(23장)과 운영이 쉬워집니다. 상태가 필요하면 명시적 핸들(예: basket_id)을 도구가 발급하고 모델이 인자로 되넘기는 패턴이 권장됩니다(2026-07-28 RC가 이 방향을 강화 — 단정 금지).
안티패턴 모음
| 안티패턴 | 왜 나쁜가 | 대신 |
|---|---|---|
stdout에 print | 프로토콜 오염(07장) | stderr / ctx.info |
| 도구를 수십 개 한 서버에 | 컨텍스트 비용·선택 혼란(18장) | 적게·응집도 높게 |
| 사용자 입력을 SQL/셸/경로에 직결 | 주입·경로 탈출(21장) | 검증·파라미터화·정규화 |
| 클라이언트 토큰 패스스루 | 혼동된 대리인(19장) | 업스트림 별도 토큰 |
| outputSchema 선언 후 미일치 반환 | 호출 에러(10장) | 스키마 일치 또는 제거 |
| 자동 무音 업데이트 리로드 | rug-pull 위험(21장) | 재검토·재동의 |
| 감사 로그 없음 | 사고 탐지 불가(23장) | 구조화 로깅·모니터링 |
좋은 MCP 서버의 특징 (종합)
- 책임이 명확하고 도구가 절제됨, 설명이 모델용 UX로 정교함.
- 입력을 엄격히 검증·정화하고, 비즈니스 실패는
isError로, 통합 오류는 예외로 구분. - 최소 권한으로 자원에 접근, 토큰 패스스루 없음, 비밀·에러 정화.
- 얇고 대체로 무상태, stateless로 확장 가능, 관측성·레이트리밋 구비.
- human-in-the-loop를 존중하고, 투명한 업데이트로 신뢰를 쌓음.
이 장에서 배운 것
- 테스트는 기능·검증·에러·스키마 + 보안 5축. 인메모리로 빠르게, Inspector로 탐색적으로, 배포 후 통합·헬스로.
- 대표 패턴은 DB·API·파일·브라우저 래핑 — 모두 "기존 시스템 위의 얇은 번역기". 각각 권한·주입·SSRF·경로 탈출을 그 자리에서 방어.
- 서버는 얇고 무상태로, 상태는 명시적 핸들로. 안티패턴(stdout print·도구 남발·입력 직결·토큰 패스스루·무음 업데이트)을 피한다.
✍️ 확인 문제
- 파일 읽기 도구를 만들었다. 테스트 5축 중 "보안" 축에서 반드시 넣어야 할 케이스 하나를 코드 아이디어로 말해 보자.
- DB 래핑 서버에서 "스키마"와 "질의 실행"을 각각 어떤 primitive로 노출하는 게 자연스러운가? 모델이 스키마를 자동으로 봐야 한다면?
- 안티패턴 표에서 셋을 골라, 각각의 올바른 대안을 한 줄로 적어 보자.
8부 끝. 다음은 부록(치트시트·SDK 빠른 참조·용어집·디버깅 체크리스트·학습 링크)으로 마무리합니다.