6.4. Pydantic 및 Zod를 활용한 코드 레벨의 스키마 정의
웹과 시스템 아키텍처 환경에서 거대 언어 모델(LLM)을 연동할 때, JSON Schema를 원시 문자열(Raw String) 형태나 딕셔너리(Dictionary)로 직접 작성하여 프롬프트에 주입하는 방식은 단기적인 프로토타이핑에는 적합할지 몰라도 확장이 불가능한 심각한 기술 부채(Technical Debt)를 야기한다. 스키마가 길어질수록 괄호의 중첩은 깊어지고, 비즈니스 형상과 모델 프롬프트 간의 버전 불일치(Version Mismatch)가 필연적으로 발생하게 된다.
이러한 문제를 원천적으로 해결하고 진정한 의미의 ’결정론적 오라클(Deterministic Oracle)’을 구현하기 위해, 현대 AI 애플리케이션 개발에서는 프로그래밍 언어 생태계 내에 깊숙이 통합된 정적 타입 시스템(Static Type System)과 런타임 유효성 검사기(Runtime Validator)를 활용한다. 본 장에서는 Python 생태계의 업계 표준인 Pydantic과 TypeScript 생태계를 주도하고 있는 Zod를 중심으로, 코드 레벨에서 스키마를 정의하고 오라클 아키텍처에 통합하는 방법을 상세히 고찰한다.
1. 단일 진실 공급원(Single Source of Truth)으로서의 코드 레벨 스키마
과거의 AI 통합 시스템들은 객체 모델을 정의하는 클래스(Class) 코드 영역과 LLM에게 구조화 출력을 지시하기 위한 JSON Schema 텍스트가 물리적으로 철저히 분리되어 있었다. 이로 인해 만약 ‘고객의 나이(Age)’ 필드를 필수 값에서 선택 값으로 변경하거나, 명칭을 ’연령’으로 수정하는 경우, 애플리케이션 백엔드 코드와 AI 프롬프트 두 곳을 모두 수정해야 했다. 이 이중 관리 구조는 필연적인 동기화 실패(Synchronization Failure)를 유발한다.
Pydantic과 Zod가 제시하는 아키텍처의 핵심은 **“코드에 작성된 타입 정보 자체가 곧 프롬프트이자 JSON Schema가 되어야 한다”**는 데 있다. 이들은 클래스나 변수의 선언부를 파싱하여 OpenAI의 Function Calling, 혹은 Structured Outputs 모드가 요구하는 JSON Schema 구조로 동적 변환(Dynamic Conversion) 해주는 강력한 기능을 제공한다.
이를 통해 개발자는 다음과 같은 근본적인 이점을 확보한다.
- D.R.Y (Don’t Repeat Yourself) 원칙 준수: 오직 하나의 언어 객체(예: Python의
BaseModel)만을 관리함으로써 프롬프트 무결성을 확보한다. - 정적 분석기(IDE/Linter)의 지원: 타입 불일치 및 오타와 같은 휴먼 에러를 런타임이 아닌 컴파일 혹은 코드 작성 시점에 사전 차단한다.
- 다형성(Polymorphism) 보장: 복수의 의도(Intent)를 가진 복잡한 구조체의 캡슐화 분리를 우아하게 설계할 수 있다.
2. Python과 Pydantic: 데이터 파싱과 메타데이터 주입
Python 생태계에서 FastAPI와 같은 모던 웹 프레임워크의 성공을 이끈 Pydantic은, AI 에이전트 개발에서도 대체 불가능한 핵심 라이브러리(De facto standard)로 자리 잡았다. Pydantic의 강점은 타입 힌트(Type Hint)를 런타임(Runtime) 환경으로 가져와 강력한 형 변환(Casting)과 유효성 검사(Validation)를 강제한다는 점이다.
LLM에 전달할 기대 출력 구조를 설계함에 있어 Pydantic의 Field 클래스는 가장 필수적인 오라클 장치다. 단순히 변수의 타입을 지정하는 것에 그치지 않고, description 매개변수를 통해 LLM이 해당 변수에 어떤 맥락의 응답을 생성해야 하는지를 지시하는 ‘마이크로 프롬프트(Micro-prompt)’ 역할을 부여할 수 있기 때문이다.
예를 들어, 리뷰에서 핵심 키워드를 추출해야 한다고 가정해 보자.
from pydantic import BaseModel, Field
class ReviewAnalysis(BaseModel):
sentiment: str = Field(
...,
description="전체 텍스트의 감성. 'positive', 'negative', 'neutral' 중 하나여야 한다."
)
keywords: list[str] = Field(
...,
min_length=1,
max_length=5,
description="리뷰에서 언급된 가장 중요한 제품의 특성 키워드 1개에서 5개 목록"
)
위와 같이 설계된 Pydantic 서브클래스는 OpenAI 인터페이스나 LangChain, Instructor 프레임워크 등을 통해 곧바로 JSON Schema로 컨버팅되어 전송된다. 만약 LLM이 6개의 키워드를 배열에 담아 응답한다면, Pydantic은 인스턴스화 시점에서 ValidationError를 발생시키며, 이 에러 메시지는 다시 모델에게 피드백으로 전달되어 자동 자가 치유(Self-healing) 프로세스를 트리거(Trigger)하는 트리 파이프라인(Tree Pipeline)을 형성한다.
3. TypeScript와 Zod: 함수형 접근과 체이닝(Chaining) 파이프라인
JavaScript 및 TypeScript 진영에서 LLM의 구조적 출력을 제어하는 표준 도구로는 Zod가 압도적인 우위를 점하고 있다. Zod는 Pydantic의 클래스 선언적 접근과 달리 개발 친화적인 함수형 합성(Functional Composition) 패턴과 체이닝(Chaining) 방식을 지향하여, 런타임의 타입 좁히기(Type Narrowing) 과정에서 발생하는 모호함을 제거한다.
Zod 스키마는 Vercel의 AI SDK나 LangChain.js와 결합하여 프론트엔드와 엣지(Edge) 환경에서의 타입 검증 오라클로 기능한다. Zod가 제공하는 .describe() 메서드 역시 Pydantic의 Field(description=...) 함수와 완벽하게 동일한 목적을 띠며, 여기에 더해 .regex(), .min(), .catch() 등 강력한 검증 빌더(Validation Builder)를 지원한다.
import { z } from "zod";
const UserExtractionSchema = z.object({
username: z.string().describe("사용자의 영문 식별 아이디"),
age: z.number().int().min(0).max(120).describe("사용자의 나이. 알 수 없는 경우 0으로 표기"),
email: z.string().email().optional().describe("사용자의 이메일 주소"),
});
위의 구조적 정의는 인젝션(Injection) 위험이나 환각(Hallucination)에 의한 오작동 코드를 방어하는 강력한 규약이다. LLM 모델이 만약 age 속성에 부동소수점을 반환하거나 ’스무 살’과 같은 텍스트를 생성할 경우, Zod 파서는 즉시 예외를 발생시키고 실행을 중단한다.
4. 요약 및 시스템 설계 시 주의점
Pydantic과 Zod를 통해 런타임에 정의된 스펙(Specification)은, AI의 예측 불가능한 거대한 지식 풀에서 애플리케이션 비즈니스 로직이라는 좁고 통제된 채널(Controlled Channel)로 정보가 유입될 때 반드시 거쳐야만 하는 톨게이트(Tollgate)다.
그러나 시스템 엔지니어들이 코드 레벨의 스키마를 설계할 때 반드시 지켜야 할 철칙이 존재한다.
- 스키마의 투명성 유지: 내부 시스템에서 사용하는 복잡한 상속이나 오버라이딩(Overriding) 구조를 LLM용 스키마에 그대로 재사용해서는 안 된다. LLM용 스키마는 철저하게 플랫(Flat)하고 설명 가능하도록 DTO(Data Transfer Object) 형태로 격리해야 한다.
- 과도한 정규식(Regex)의 자제: Pydantic과 Zod 모두 패턴 매칭을 제공하지만, 복잡한 정규식은 모델 내부의 구조화 로직(예: Constrained Decoding)을 혼란스럽게 하여 생성 자체를 실패하게 만들 위험이 있다.
- Fail-Fast 철학: 타입 오류나 유효성 규칙 위반이 발생하면 결코 데이터를 억지로 보완(Coercion)하여 다음 프로세스로 넘기지 말고, 가차 없이 오류 이벤트를 발생시켜 인간의 개입이나 AI 재시도 로직으로 이관 시켜야 한다.