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;
  1. 기능: 유효 입력에 올바른 결과를 내는가.
  2. 검증: 타입·범위 위반 입력이 본문 전에 걸러지는가(15장).
  3. 에러: 비즈니스 실패가 예외가 아니라 isError로 오는가(16장).
  4. 스키마: outputSchema를 선언했으면 structuredContent가 맞는가(10장).
  5. 보안: 경로 탈출(../../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 fastmcpfrom_openapi 등 — 버전 확인). OpenAPI는 REST API의 엔드포인트·인자·응답을 기계가 읽을 수 있게 기술하는 표준 명세 형식으로, 이 명세가 있으면 도구 정의를 자동으로 뽑아낼 수 있습니다.
  • 🔒 업스트림엔 별도 토큰: 클라이언트 토큰을 패스스루하지 말 것(19장, confused deputy). 외부 요청 목적지를 허용목록화해 SSRF 차단(21장).
  • 큰 응답은 요약·페이지네이션으로 컨텍스트 비용 절감(18장).

3) 파일시스템 래핑

  • 파일 읽기는 Resource(URI 템플릿)나 Tool로. 🔒 경로 정규화·경계 검사 필수(15장)로 경로 탈출 차단. roots로 접근 범위를 받습니다(13장).

4) 브라우저·자동화 래핑

  • 웹 조작·스크래핑을 도구로. 부작용·외부 송신이 크므로 🔒 human-in-the-loop와 도메인 허용목록을 강하게 겁니다(20장, 21장).

서버는 얇게, 대체로 무상태

반복되는 설계 원칙: 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·도구 남발·입력 직결·토큰 패스스루·무음 업데이트)을 피한다.

✍️ 확인 문제

  1. 파일 읽기 도구를 만들었다. 테스트 5축 중 "보안" 축에서 반드시 넣어야 할 케이스 하나를 코드 아이디어로 말해 보자.
  2. DB 래핑 서버에서 "스키마"와 "질의 실행"을 각각 어떤 primitive로 노출하는 게 자연스러운가? 모델이 스키마를 자동으로 봐야 한다면?
  3. 안티패턴 표에서 셋을 골라, 각각의 올바른 대안을 한 줄로 적어 보자.
8부 끝. 다음은 부록(치트시트·SDK 빠른 참조·용어집·디버깅 체크리스트·학습 링크)으로 마무리합니다.