AI 도구 하드코딩 중단하기: Zod 및 MCP를 활용한 동적 도구 검색 및 스키마 검증
요약
AI 에이전트 개발 시 도구 정의를 하드코딩하는 대신, MCP와 Zod를 활용하여 동적으로 도구를 검색하고 스키마를 검증하는 아키텍처를 제안합니다. 이를 통해 외부 API 변경에 유연하게 대응하고 시스템의 안정성을 높이는 엔터프라이즈급 분산 패턴을 다룹니다.
핵심 포인트
- 정적 도구 바인딩의 취약성과 모놀리식 구조의 한계 지적
- Model Context Protocol(MCP)을 통한 동적 도구 검색 활용
- Zod를 이용한 런타임 스키마 검증으로 LLM 환각 방지
- 마이크로서비스 아키텍처 개념을 AI 에이전트에 적용
만약 당신이 여전히 AI 에이전트 초기화 스크립트에 도구 정의(tool definitions), JSON 스키마(JSON schemas), 그리고 수동 라우팅 로직(manual routing logic)을 직접 하드코딩하고 있다면, 당신은 모래 위에 성을 쌓고 있는 것입니다.
수년 동안, 에이전트 프레임워크(agentic frameworks)의 초기 반복 버전들은 개발자들이 핵심 애플리케이션 루프(application loop)에 직접 기능을 하드코딩하도록 강제했습니다. 이러한 정적 패러다임(static paradigm)은 모든 HTML 페이지, 스크립트 태그, 스타일시트 경로를 수동으로 선언하고 모놀리식 바이너리(monolithic binaries)로 컴파일해야 했던 웹 개발의 초기 시절을 반영합니다. 하지만 현대적인 시스템이 분산된 에이전트 메시 네트워크(agentic mesh networks)를 향해 확장됨에 따라, 정적 도구 바인딩(static tool binding)은 취약한 아키텍처를 생성합니다. 외부 API 스키마가 업데이트되거나 새로운 마이크로서비스(microservice)가 가동되는 순간, 당신의 에이전트 전체가 무너집니다.
해결책은 무엇일까요? 정적 프롬프트 엔지니어링(prompt engineering)에서 벗어나 **Model Context Protocol (MCP)**과 **Zod 런타임 스키마 검증 (runtime schema validation)**을 결합하는 패러다임의 전환입니다.
이 심층 분석에서는 AI 에이전트를 구축하는 기존의 방식들을 해체하고, 엔터프라이즈급 분산 패턴(distributed patterns)을 사용하여 이를 재구축할 것입니다. 당신은 거대 언어 모델 (LLM) 추론 엔진을 외부 기능으로부터 분리하는 방법, 동적 런타임 도구 검색(dynamic runtime tool discovery)을 활용하는 방법, LLM의 환각(hallucinations)이 데이터베이스를 파괴하는 것을 방지하는 방법, 그리고 TypeScript에서 병렬 도구 호출(parallel tool calls)을 안전하게 실행하는 방법을 배우게 될 것입니다.
마이크로서비스 비유: 왜 정적 AI 아키텍처는 실패하는가
현대적인 AI 엔지니어링에서 왜 동적 도구 검색(dynamic tool discovery)이 필수적인지 이해하기 위해, 전통적인 소프트웨어 아키텍처를 살펴보겠습니다.
모든 데이터베이스 쿼리, 제3자 결제 게이트웨이(payment gateway), 그리고 알림 서비스가 하나의 거대하고 방대한 코드베이스에 밀집되어 있는 모놀리식 웹 애플리케이션을 상상해 보십시오. 만약 결제 게이트웨이가 API 페이로드(payload)를 문자열 기반 통화에서 정수 기반의 최소 단위(minor-unit) 형식으로 업데이트한다면, 당신의 모놀리스(monolith) 전체를 다시 컴파일하고 재배포해야 합니다.
이제 현대적인 클라우드 네이티브(cloud-native) 패러다임인 **마이크로서비스 (Microservices)**를 살펴보십시오. 마이크로서비스 메시(microservice mesh) 내에서 서비스들은 다운스트림 의존성(downstream dependencies)의 내부 데이터 구조를 하드코딩하지 않습니다. 대신, 서비스 디스커버리 (service discovery) 프로토콜(Consul 또는 Kubernetes DNS와 같은)과 엄격한 인터페이스 계약(OpenAPI 또는 gRPC/Protobuf와 같은)에 의존합니다. 서비스가 시작될 때, 레지스트리(registry)를 쿼리하여 사용 가능한 엔드포인트(endpoint)를 발견하고, 해당 스키마(schema)를 가져오며, _런타임 (runtime)_에 해당 스키마를 기준으로 들어오고 나가는 페이로드(payload)를 검증합니다.
**모델 컨텍스트 프로토콜 (Model Context Protocol, MCP)**은 이와 정확히 일치하는 마이크로서비스 토폴로지(topology)를 AI 에이전트에 적용합니다.
데이터 페칭(data fetching), 도구 라우팅(tool routing), 모델 상호작용을 포함한 복잡한 로직이 완전히 서버 컴포넌트(Server Components)와 서버 액션(Server Actions) 내에 존재하는 현대적인 AI 챗봇 아키텍처에서, LLM은 분산 시스템의 오케스트레이터(orchestrator) 역할을 수행합니다. LLM은 세상에 어떤 도구들이 존재하는지 본질적으로 알지 못합니다. LLM은 오직 현재의 실행 컨텍스트 윈도우(execution context window) 내에서 자신에게 제시된 도구들만이 무엇인지 알 뿐입니다.
도구 정의를 외부 MCP 서버로 오프로딩(offloading)함으로써, 당신의 에이전트는 초기화 시점이나 심지어 실행 중간에도 이러한 서버들을 쿼리하여 새롭게 사용 가능해진 기능들을 발견할 수 있습니다. 만약 사용자가 새로운 데이터베이스 커넥터나 브라우저 자동화 확장을 연결한다면, MCP 서버는 업데이트된 기능 매니페스트(capability manifest)를 브로드캐스트(broadcast)합니다. 에이전트는 이 매니페스트를 동적으로 파싱(parse)하고, 구조적 정의를 수용하며, 핵심 애플리케이션 런타임을 단 한 번도 재시작할 필요 없이 내부 라우팅 테이블(routing table)을 업데이트합니다.
타입이 지정되지 않은 환각의 위험성: 스키마 검증이 중요한 이유
동적 디스커버리(dynamic discovery)가 전례 없는 유연성을 제공하는 반면, 이는 심각한 보안 취약점인 _잘못된 형식의 실행 벡터 (malformed execution vector)_를 유발합니다.
LLM은 확률론적인 토큰 예측 엔진입니다. 이들은 본질적으로 구문 드리프트(syntax drift), 파라미터 이름의 환각(hallucination), 그리고 타입 강제 변환(type-coercion) 오류에 취약합니다.
만약 LLM이 execute_database_query라는 이름의 동적으로 검색된 도구를 호출하기로 결정한다면, 단순한 에이전트 프레임워크(naive agent framework)는 모델이 생성한 JSON 객체를 가져와 실행 계층(execution layer)으로 직접 전달할 것입니다. 만약 모델이 정수(integer)가 필요한 곳에 문자열(string)을 전달하거나, 확률적 주의력 저하(stochastic attention degradation)로 인해 필수적인 connectionString 파라미터를 완전히 누락시킨다면, 다운스트림 데이터베이스 드라이버(downstream database driver)는 처리되지 않은 예외를 발생시키거나, 상태를 손상시키거나, 최악의 경우 인젝션 취약점(injection vulnerabilities)을 통해 의도하지 않은 작업을 실행할 수도 있습니다.
이 지점에서 Zod와 MCP의 결합은 깨지지 않는 런타임 계약(runtime contract)을 생성합니다.
TypeScript 생태계에서 컴파일 타임 타입(compile-time types, 예: interface 또는 type)은 JavaScript 컴파일 과정에서 완전히 사라집니다. 이들은 오직 IDE와 TypeScript 컴파일러(tsc)를 위해서만 존재합니다. 런타임(runtime)에서 JavaScript는 아무런 정보 없이 실행됩니다.
Zod는 **런타임 스키마 검증 (runtime schema validation)**을 제공함으로써 이 간극을 메웁니다. Zod 스키마는 단순한 타입 선언이 아닙니다. 이는 애플리케이션의 경계에서 알 수 없는 데이터를 검사하고, 실패 시 설명적인 에러를 발생시키며, 지능적인 타입 캐스팅(type casting) 및 새니타이제이션(sanitization)을 수행하는 일급 실행 객체(first-class executable object)입니다.
MCP 서버가 도구를 노출할 때, 도구에 대한 사람이 읽을 수 있는 설명과 공식적인 구조적 스키마(formal structural schema)를 함께 노출합니다. 클라이언트 측(Server Actions 또는 LangGraph 노드 내부)에서 이 스키마는 수집되어 Zod 검증 객체로 컴파일됩니다.
어떠한 도구 실행 페이로드(tool execution payload)도 외부 API나 파일 시스템에 닿기 전에, 반드시 Zod 검증 게이트(Zod validation gate)를 통과해야 합니다. 만약 LLM이 잘못된 형식의 인자(argument)를 생성하면, Zod 파서(parser)가 페이로드를 가로채 검증 에러를 포착하고, 정밀한 에러 피드백 루프를 구조화된 프롬프트 교정(structured prompt correction)의 형태로 LLM에 직접 다시 전달합니다. 이는 치명적인 런타임 충돌을 자가 치유가 가능한 에이전트 루프(self-healing agentic loop)로 변환합니다.
MCP에서의 비동기 도구 처리 및 병렬 실행
복잡한 에이전트 워크플로(agentic workflows)—특히 웹 자동화, 멀티 테넌트(multi-tenant) SaaS 대시보드, 또는 분산 데이터 집계(distributed data aggregation)를 포함하는 경우—에서는 단일 사용자 프롬프트가 모델로 하여금 5개의 서로 다른 엔드포인트에서 데이터를 가져오고, 3개의 서로 다른 파일을 파싱하며, 브라우저 자동화 스크립트를 동시에 시작하도록 요구할 수 있습니다.
동기식(synchronous) 또는 단일 스레드(single-threaded) 실행 모델에서는 이것이 치명적인 성능 병목 현상(performance bottleneck)을 초래합니다. 만약 에이전트가 도구를 순차적으로 실행한다면—도구 A를 호출하고, 응답을 기다리고, 도구 B를 호출하고, 응답을 기다리는 방식—지연 시간(latency)이 선형적으로 누적되어 타임아웃(timeout)과 사용자 경험 저하로 이어집니다.
엔터프라이즈 아키텍처에서 확립된 바와 같이, 병렬 도구 실행 (Parallel Tool Execution) 및 **비동기 도구 처리 (Asynchronous Tool Handling)**는 필수적인 설계 패턴입니다. MCP 명세 내에서 전송 계층(transport layers, 예: Server-Sent Events 또는 표준 입출력 채널)은 완전히 비동기적이며 멀티플렉싱(multiplexed)됩니다. 이는 MCP 클라이언트가 이벤트 루프(event loop)를 차단하지 않고도 여러 개의 JSON-RPC 도구 호출 요청을 네트워크를 통해 동시에 전송할 수 있음을 의미합니다.
LLM이 단일 턴(turn)에서 여러 도구 호출을 포함하는 응답을 출력할 때, 에이전트 실행 노드는 JavaScript의 네이티브 비동기 동시성 프리미티브(asynchronous concurrency primitives, 예: Promise.allSettled 또는 Promise.all)를 활용하여 이러한 요청들을 각각의 MCP 서버로 병렬로 전달해야 합니다.
하지만 병렬 실행은 동시성 위험(concurrency hazards), 경합 조건(race conditions), 그리고 상태 동기화(state synchronization) 문제를 야기합니다. 만약 두 도구가 적절한 격리(isolation) 없이 동일한 임시 파일에 쓰기를 시도하거나 동일한 공유 에이전트 상태 객체를 변경(mutate)하려고 한다면, 시스템은 데이터 손상(data corruption)을 겪게 될 것입니다. 따라서 MCP 클라이언트 런타임은 병렬 도구 실행 전반에 걸쳐 엄격한 불변성 경계(immutability boundaries)를 강제해야 하며, 비동기 집계(asynchronous aggregation) 단계가 완료될 때까지 각 도구가 샌드박스된 컨텍스트(sandboxed context) 내에서 작동하거나 불변 상태 스냅샷(immutable state snapshots)을 대상으로 작동하도록 보장해야 합니다.
클라이언트 경계 계약의 철학
이 아키텍처의 깊이를 온전히 이해하려면, MCP 경계(boundary)가 나타내는 철학적 변화를 이해해야 합니다.
전통적인 클라이언트-서버 웹 애플리케이션에서는 서버를 신뢰할 수 있는 대상으로, 클라이언트를 신뢰할 수 없는 대상으로 간주합니다. 서버는 엔드포인트(endpoints)를 노출하고, 클라이언트는 서버가 검증할 페이로드(payloads)를 전송합니다.
현대적인 웹 프레임워크 내에서 실행되는 AI 에이전트 아키텍처에서는 이 신뢰 경계가 **역전(inverts)**됩니다.
LLM은 초월적이고 확률적인 클라우드 서비스(예: OpenAI, Anthropic 또는 로컬 가중치(local weights))에 거주하는 반면, 우리의 결정론적인 비즈니스 로직(deterministic business logic)은 우리의 서버 환경(Next.js Server Actions, Node.js workers, LangGraph nodes) 내에 존재합니다. LLM은 사실상 신뢰할 수 없는, 매우 지능적인 인턴과 같습니다. 지침을 읽고, 의도를 이해하며, 계획을 초안할 수는 있지만, 엄격한 감독 없이는 정확한 구문(syntax)을 입력하거나, 엄격한 데이터 타입(data types)을 기억하거나, 결정론적인 규칙을 준수한다고 신뢰할 수 없습니다.
Zod 스키마 검증(schema validation)과 결합된 Model Context Protocol은 이 인턴을 위한 조직적 프로토콜이자 안전 매뉴얼 역할을 합니다:
- **MCP 서버(The MCP Server)**는 부서 창고 역할을 하며, 사용 가능한 모든 도구와 그에 필요한 요청 양식(schemas)을 목록화합니다.
- **동적 검색 단계(The Dynamic Discovery Phase)**는 인턴이 오늘 어떤 도구를 사용할 수 있는지 배우는 아침 브리핑 역할을 합니다.
- LLM은 계획을 세우고 요청 양식을 작성하는 인턴 역할을 합니다.
- **Zod 검증 게이트(The Zod Validation Gate)**는 창고 문에 배치된 엄격한 준법 감시관 역할을 합니다. 만약 양식에 오타가 있거나, 누락된 필드가 있거나, 잘못된 데이터 타입이 있다면, 준법 감시관은 즉시 이를 거부하고 정확한 오류를 설명하는 빨간 펜을 건네주며, 인턴이 창고 내부의 기계를 고장 내는 것을 방지합니다.
실전 구현: Zod를 활용한 동적 MCP 클라이언트 구축
TypeScript에서 동적 도구 검색(dynamic tool discovery)과 런타임 스키마 검증(runtime schema validation)을 구현하는 방법을 살펴보겠습니다. 아래는 에이전트가 사용자 조회(user-lookup) 도구를 동적으로 검색하고 실행하는 SaaS 고객 지원 환경을 시뮬레이션한, 독립 실행 가능한 프로덕션급 예제입니다.
import { z } from "zod";
/**
...
구현 코드의 라인별 상세 분석
import { z } from "zod";: 엄격한 런타임 검증 경계(runtime validation boundaries)를 설정하기 위해 Zod를 임포트합니다. 이를 통해 신뢰할 수 없는 LLM 출력이 사전 검사 없이 민감한 애플리케이션 로직에 닿는 것을 방지합니다.interface MCPToolDefinition: 원격 도구를 위한 깔끔한 구조를 정의하며, 도구의 이름, LLM 프롬프트용 설명 문서, Zod 입력 스키마(input schema), 그리고 실행 핸들러(execution handler)를 캡슐화합니다.class MCPServerRegistry: 원격 마이크로서비스 레지스트리(microservice registry)의 인메모리 모의(in-memory mock) 역할을 수행합니다. 활성 도구 매니페스트(tool manifests)를 유지 관리하며, Zod 스키마를 MCP 와이어 프로토콜(wire protocols)을 통한 네트워크 전송에 적합한 JSON 스키마(JSON Schema) 형식으로 변환합니다.class MCPSaaSClientAgent: 애플리케이션 레이어 내부에서 동작하는 런타임을 나타냅니다. 서버에 기능(capabilities)을 질의하고 검증 핸드셰이크(validation handshake)를 처리합니다.safeParse(rawArguments): 핵심 보안 체크포인트입니다. LLM의 가공되지 않은 출력을 그대로 신뢰하는 대신, Zod가 요구되는 스키마에 따라 데이터를 평가하여 네트워크 전송 전에 타입 불일치, 누락된 속성 또는 잘못된 형식을 잡아냅니다.
TypeScript를 통한 엔드-투-엔드 타입 안전성(Type Safety) 강제
전통적인 TypeScript 애플리케이션에서의 타입 안전성은 일반적으로 자체 코드베이스 경계에서 멈춥니다. 외부 HTTP 요청을 보내거나, 디스크에서 파일을 읽거나, 제3자 LLM으로부터 데이터를 받는 순간, TypeScript의 컴파일 타임 보장(compile-time guarantees)은 사라지고 위험한 any 또는 unknown 타입으로 대체됩니다.
Zod를 우리의 MCP 아키텍처에 통합함으로써, 우리는 타입 안전성(type safety)을 컴파일 타임(compile-time) 경계를 넘어 **런타임 실행 경계(runtime execution boundary)**로 직접 확장합니다. Zod 스키마는 z.infer<typeof schema>를 통해 정적 TypeScript 타입을 자동으로 추론할 수 있기 때문에, 우리는 끊김 없이 연속적인 안전 파이프라인을 달성할 수 있습니다:
- 원격 MCP 서버 정의 (Remote MCP Server Definition): 서버가 도구 입력(tool inputs)을 정의합니다.
- 전송 (Transport Transmission): 스키마가 JSON Schema로 직렬화되어 JSON-RPC를 통해 전송됩니다.
- 클라이언트 측 수집 (Client-Side Ingestion): 클라이언트가 JSON Schema 메타데이터를 수신합니다.
- Zod 컴파일 (Zod Compilation): 클라이언트가 런타임 Zod 검증기(validator) 객체를 생성합니다.
- 타입 추론 (Type Inference): TypeScript가
z.infer를 사용하여 Zod 스키마로부터 정확한 정적 타입을 추론합니다. - 실행 및 검증 (Execution & Validation): 런타임이 들어오는 LLM 인자(arguments)를 Zod 스키마에 따라 검증하여, 정적 타입이 실행 시점의 데이터 물리적 형태(physical shape)와 일치하도록 보장합니다.
이는 "타입 불일치 버그(type divergence bugs)"—개발자가 백엔드 타입 정의는 업데이트했지만 프론트엔드 파싱 로직 업데이트를 잊어버리거나, LLM이 타입이 지정되지 않은 JavaScript 체크를 통과하는 속성을 환각(hallucinate)하여 발생하는 교활한 오류 유형—를 완전히 제거합니다.
마치며: 엔터프라이즈급 AI 엔지니어링으로의 전환
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기