본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 4장: Pydantic과 타입 안전 출력
2026년 3월 26일·AI / ML·

4장: Pydantic과 타입 안전 출력

Pydantic v2로 LLM 출력 스키마를 정의하고, Instructor 라이브러리로 자동 재시도와 스트리밍 구조화 출력을 구현합니다.

15분929자7개 섹션
structured-outputaidata-engineeringllm
공유
structured-output4 / 10
12345678910
이전3장: 함수 호출(Function Calling)과 도구 사용다음5장: 비정형 데이터에서 구조화된 정보 추출

학습 목표

  • Pydantic v2 모델로 LLM 출력 스키마를 정의하고 JSON Schema를 자동 생성합니다
  • Instructor 라이브러리로 멀티 프로바이더 구조화 출력과 자동 재시도를 구현합니다
  • Pydantic AI의 3가지 출력 방식(Tool/Native/Prompted)을 비교합니다
  • TypeScript 환경에서 Zod를 활용한 구조화 출력 방법을 학습합니다

Pydantic v2와 JSON Schema

Pydantic은 Python의 데이터 검증 라이브러리입니다. v2에서는 Rust 기반 코어(pydantic-core)를 도입하여 성능이 크게 향상되었으며, JSON Schema 생성 기능이 내장되어 있어 LLM 구조화 출력과 자연스럽게 결합됩니다.

모델 정의

models.py
python
from pydantic import BaseModel, Field
from enum import Enum
 
 
class Sentiment(str, Enum):
    POSITIVE = "positive"
    NEGATIVE = "negative"
    NEUTRAL = "neutral"
 
 
class AspectSentiment(BaseModel):
    """개별 측면의 감성 분석 결과"""
    aspect: str = Field(description="분석 대상 측면 (예: 배송, 포장, 가격)")
    sentiment: Sentiment = Field(description="해당 측면의 감성")
    evidence: str = Field(description="감성 판단의 근거가 되는 원문 발췌")
 
 
class ReviewAnalysis(BaseModel):
    """리뷰 감성 분석의 전체 결과"""
    overall_sentiment: Sentiment = Field(description="전체적인 감성 판단")
    confidence: float = Field(
        ge=0.0, le=1.0,
        description="감성 판단 신뢰도 (0.0 - 1.0)"
    )
    aspects: list[AspectSentiment] = Field(
        description="측면별 세부 감성 분석",
        min_length=1
    )
    summary: str = Field(
        max_length=200,
        description="분석 결과를 한두 문장으로 요약"
    )

JSON Schema 자동 생성

Pydantic 모델에서 JSON Schema를 생성하는 것은 한 줄이면 충분합니다.

schema_generation.py
python
import json
 
schema = ReviewAnalysis.model_json_schema()
print(json.dumps(schema, indent=2, ensure_ascii=False))
generated_schema.json
json
{
  "title": "ReviewAnalysis",
  "description": "리뷰 감성 분석의 전체 결과",
  "type": "object",
  "properties": {
    "overall_sentiment": {
      "description": "전체적인 감성 판단",
      "enum": ["positive", "negative", "neutral"],
      "title": "Overall Sentiment",
      "type": "string"
    },
    "confidence": {
      "description": "감성 판단 신뢰도 (0.0 - 1.0)",
      "maximum": 1.0,
      "minimum": 0.0,
      "title": "Confidence",
      "type": "number"
    },
    "aspects": {
      "description": "측면별 세부 감성 분석",
      "items": {"$ref": "#/$defs/AspectSentiment"},
      "minItems": 1,
      "title": "Aspects",
      "type": "array"
    },
    "summary": {
      "description": "분석 결과를 한두 문장으로 요약",
      "maxLength": 200,
      "title": "Summary",
      "type": "string"
    }
  },
  "required": ["overall_sentiment", "confidence", "aspects", "summary"]
}
Info

Pydantic의 Field에 설정한 description, ge, le, max_length, min_length 등의 제약 조건이 JSON Schema로 자동 변환됩니다. 이 메타데이터는 LLM이 올바른 값을 생성하는 데 중요한 가이드 역할을 합니다.

복합 타입과 유니온

실무에서 자주 사용하는 복합 타입 패턴입니다.

complex_types.py
python
from pydantic import BaseModel, Field
from typing import Literal
 
 
class TextEntity(BaseModel):
    """텍스트 엔티티"""
    entity_type: Literal["person", "organization", "location", "date"]
    value: str
    start_offset: int = Field(ge=0, description="원문에서의 시작 위치")
    end_offset: int = Field(ge=0, description="원문에서의 끝 위치")
 
 
class Relationship(BaseModel):
    """엔티티 간 관계"""
    subject: str = Field(description="주체 엔티티")
    predicate: str = Field(description="관계 유형 (예: works_at, located_in)")
    obj: str = Field(description="대상 엔티티")
 
 
class DocumentAnalysis(BaseModel):
    """문서 분석 결과"""
    title: str
    language: Literal["ko", "en", "ja", "zh"]
    entities: list[TextEntity]
    relationships: list[Relationship]
    key_facts: list[str] = Field(
        max_length=10,
        description="문서의 핵심 사실 (최대 10개)"
    )
    word_count: int = Field(ge=0)

Instructor 라이브러리

Instructor는 Pydantic 모델을 LLM 구조화 출력과 연결하는 오픈소스 라이브러리입니다. 자동 재시도, 스트리밍, 멀티 프로바이더 지원 등 프로덕션에 필요한 기능을 제공합니다.

설치 및 기본 사용

terminal
bash
pip install instructor
instructor_basic.py
python
import instructor
from openai import OpenAI
from pydantic import BaseModel, Field
from enum import Enum
 
 
class Priority(str, Enum):
    CRITICAL = "critical"
    HIGH = "high"
    MEDIUM = "medium"
    LOW = "low"
 
 
class TicketClassification(BaseModel):
    """고객 문의 티켓 분류 결과"""
    category: str = Field(description="문의 카테고리 (예: 결제, 배송, 반품)")
    priority: Priority = Field(description="우선순위")
    sentiment: str = Field(description="고객 감정 상태")
    suggested_action: str = Field(description="권장 조치 사항")
    requires_escalation: bool = Field(description="상위 담당자 에스컬레이션 필요 여부")
 
 
# Instructor로 OpenAI 클라이언트 패치
client = instructor.from_openai(OpenAI())
 
ticket = client.chat.completions.create(
    model="gpt-4o-2026-02",
    response_model=TicketClassification,
    messages=[
        {
            "role": "user",
            "content": "결제가 이중으로 되었는데 3일째 환불이 안 됩니다. 정말 화가 납니다!"
        }
    ]
)
 
print(ticket.model_dump_json(indent=2))

Instructor의 핵심은 response_model 매개변수입니다. Pydantic 모델을 전달하면, 내부적으로 JSON Schema를 추출하여 LLM API에 전달하고, 응답을 Pydantic 인스턴스로 변환합니다.

자동 재시도

LLM 출력이 Pydantic 검증을 통과하지 못하면 자동으로 재시도합니다. 재시도 시 검증 오류 메시지를 컨텍스트로 함께 전달하여 LLM이 오류를 수정할 수 있게 합니다.

instructor_retry.py
python
import instructor
from openai import OpenAI
from pydantic import BaseModel, Field, field_validator
 
 
class ExtractedAmount(BaseModel):
    """금액 추출 결과"""
    currency: str = Field(description="통화 코드 (ISO 4217)")
    amount: float = Field(gt=0, description="금액 (양수)")
    original_text: str = Field(description="원문에서 금액이 표현된 부분")
 
    @field_validator("currency")
    @classmethod
    def validate_currency(cls, v: str) -> str:
        valid_currencies = {"KRW", "USD", "EUR", "JPY", "GBP", "CNY"}
        if v not in valid_currencies:
            raise ValueError(
                f"지원하지 않는 통화입니다: {v}. "
                f"지원 통화: {', '.join(sorted(valid_currencies))}"
            )
        return v
 
 
client = instructor.from_openai(OpenAI())
 
result = client.chat.completions.create(
    model="gpt-4o-2026-02",
    response_model=ExtractedAmount,
    max_retries=3,  # 최대 3회 재시도
    messages=[
        {
            "role": "user",
            "content": "이번 달 전기요금이 45,000원 나왔습니다."
        }
    ]
)
 
print(result)
# currency='KRW' amount=45000.0 original_text='45,000원'
Warning

max_retries를 높게 설정하면 비용과 지연 시간이 증가합니다. 일반적으로 2-3회가 적절하며, 그 이상 재시도가 필요하다면 스키마나 프롬프트를 개선하는 것이 바람직합니다.

멀티 프로바이더 지원

Instructor는 다양한 LLM 프로바이더를 동일한 인터페이스로 지원합니다.

instructor_multi_provider.py
python
import instructor
from openai import OpenAI
from anthropic import Anthropic
from pydantic import BaseModel, Field
 
 
class Summary(BaseModel):
    """텍스트 요약 결과"""
    title: str = Field(description="요약 제목")
    bullet_points: list[str] = Field(description="핵심 내용 (3-5개)")
    word_count: int = Field(description="원문 단어 수 추정")
 
 
# OpenAI
openai_client = instructor.from_openai(OpenAI())
 
# Anthropic
anthropic_client = instructor.from_anthropic(Anthropic())
 
# 동일한 인터페이스로 호출
text = "분석할 긴 텍스트..."
 
# OpenAI로 요약
openai_result = openai_client.chat.completions.create(
    model="gpt-4o-2026-02",
    response_model=Summary,
    messages=[{"role": "user", "content": f"다음 텍스트를 요약하세요: {text}"}]
)
 
# Anthropic으로 요약
anthropic_result = anthropic_client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    response_model=Summary,
    messages=[{"role": "user", "content": f"다음 텍스트를 요약하세요: {text}"}]
)

스트리밍 구조화 출력

Instructor는 **Partial Streaming(부분 스트리밍)**을 지원합니다. LLM이 토큰을 생성할 때마다 부분적인 Pydantic 객체를 반환하여, 사용자에게 실시간 피드백을 제공할 수 있습니다.

instructor_streaming.py
python
import instructor
from openai import OpenAI
from pydantic import BaseModel, Field
 
 
class ArticleOutline(BaseModel):
    """기사 개요"""
    title: str = Field(description="기사 제목")
    sections: list[str] = Field(description="섹션 목록")
    estimated_length: int = Field(description="예상 분량 (단어)")
 
 
client = instructor.from_openai(OpenAI())
 
# 스트리밍 모드로 호출
stream = client.chat.completions.create_partial(
    model="gpt-4o-2026-02",
    response_model=ArticleOutline,
    messages=[
        {
            "role": "user",
            "content": "AI가 교육에 미치는 영향에 대한 기사 개요를 작성하세요."
        }
    ]
)
 
for partial in stream:
    # 부분적으로 채워진 Pydantic 객체
    print(f"제목: {partial.title}")
    print(f"섹션 수: {len(partial.sections)}")
    print("---")

Pydantic AI의 3가지 출력 방식

Pydantic AI는 Pydantic 팀이 공식 개발한 AI 에이전트 프레임워크입니다. 구조화된 출력을 생성하는 세 가지 방식을 제공합니다.

1. Tool Output

도구 호출을 통해 구조화된 출력을 생성합니다. 기본 방식이며 대부분의 모델에서 안정적으로 동작합니다.

pydantic_ai_tool.py
python
from pydantic_ai import Agent
from pydantic import BaseModel, Field
 
 
class CityInfo(BaseModel):
    """도시 정보"""
    name: str
    country: str
    population: int = Field(description="인구 수 (추정)")
    famous_for: list[str] = Field(description="유명한 것들")
 
 
agent = Agent(
    "openai:gpt-4o",
    output_type=CityInfo
)
 
result = agent.run_sync("서울에 대해 알려주세요.")
print(result.output)
# name='서울' country='대한민국' population=9700000 famous_for=['경복궁', 'N서울타워', ...]

2. Native Output

모델의 네이티브 Structured Output 기능을 사용합니다. OpenAI의 response_format 같은 기능을 직접 활용합니다.

pydantic_ai_native.py
python
from pydantic_ai import Agent
from pydantic_ai.output import NativeOutput
from pydantic import BaseModel
 
 
class MathSolution(BaseModel):
    """수학 문제 풀이"""
    reasoning: str
    answer: float
    unit: str
 
 
agent = Agent(
    "openai:gpt-4o",
    output_type=NativeOutput(MathSolution)
)
 
result = agent.run_sync("원의 넓이를 구하세요. 반지름은 5cm입니다.")
print(result.output)

3. Prompted Output

모델에 JSON 형식을 프롬프트로 주입하는 방식입니다. 네이티브 구조화 출력이나 도구 호출을 지원하지 않는 모델에서 사용합니다.

pydantic_ai_prompted.py
python
from pydantic_ai import Agent
from pydantic_ai.output import PromptedOutput
from pydantic import BaseModel
 
 
class Translation(BaseModel):
    """번역 결과"""
    original: str
    translated: str
    source_language: str
    target_language: str
 
 
agent = Agent(
    "openai:gpt-4o",
    output_type=PromptedOutput(Translation)
)
 
result = agent.run_sync("Translate 'Hello World' to Korean.")
print(result.output)

TypeScript: Zod를 활용한 구조화 출력

TypeScript 환경에서는 Zod 스키마 라이브러리가 Pydantic과 유사한 역할을 합니다. OpenAI의 Node.js SDK는 Zod를 공식 지원합니다.

zod_structured.ts
typescript
import OpenAI from "openai";
import { z } from "zod";
import { zodResponseFormat } from "openai/helpers/zod";
 
const ReviewAnalysis = z.object({
  sentiment: z.enum(["positive", "negative", "neutral"])
    .describe("전체적인 감성"),
  confidence: z.number().min(0).max(1)
    .describe("감성 판단 신뢰도"),
  aspects: z.array(
    z.object({
      aspect: z.string().describe("분석 대상 측면"),
      sentiment: z.enum(["positive", "negative", "neutral"]),
    })
  ).describe("측면별 감성 분석"),
  summary: z.string().max(200).describe("분석 요약"),
});
 
type ReviewAnalysisType = z.infer<typeof ReviewAnalysis>;
 
const client = new OpenAI();
 
async function analyzeReview(text: string): Promise<ReviewAnalysisType> {
  const completion = await client.beta.chat.completions.parse({
    model: "gpt-4o-2026-02",
    messages: [
      { role: "system", content: "리뷰를 분석하세요." },
      { role: "user", content: text },
    ],
    response_format: zodResponseFormat(ReviewAnalysis, "review_analysis"),
  });
 
  const result = completion.choices[0].message.parsed;
  if (!result) throw new Error("파싱 실패");
  return result;
}
Tip

Zod의 z.infer를 사용하면 스키마에서 TypeScript 타입을 자동으로 추론할 수 있습니다. 스키마와 타입이 항상 동기화되므로 타입 불일치 문제가 발생하지 않습니다.


정리

이번 장에서는 Pydantic과 Zod를 활용한 타입 안전 구조화 출력을 학습했습니다.

핵심 내용을 정리하면 다음과 같습니다.

  • Pydantic v2 모델은 model_json_schema()로 JSON Schema를 자동 생성하며, Field의 메타데이터가 LLM의 출력 품질을 좌우합니다
  • Instructor 라이브러리는 자동 재시도, 스트리밍, 멀티 프로바이더를 지원하여 프로덕션 수준의 구조화 출력을 가능하게 합니다
  • Pydantic AI는 Tool, Native, Prompted 세 가지 출력 방식을 제공하여 다양한 모델을 지원합니다
  • TypeScript 환경에서는 Zod와 OpenAI SDK의 zodResponseFormat을 활용합니다

다음 장 미리보기

5장에서는 비정형 데이터(PDF, 이미지, 웹페이지)에서 구조화된 정보를 추출하는 실전 기법을 다룹니다. OCR과 LLM을 결합한 파이프라인, 테이블 추출, 엔티티-관계 추출 등을 학습합니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#structured-output#ai#data-engineering#llm

관련 글

AI / ML

5장: 비정형 데이터에서 구조화된 정보 추출

PDF, 이미지, 웹페이지 등 비정형 데이터에서 LLM을 활용하여 구조화된 정보를 추출하는 실전 기법을 학습합니다.

2026년 3월 28일·18분
AI / ML

3장: 함수 호출(Function Calling)과 도구 사용

Function Calling의 원리를 이해하고, OpenAI/Anthropic/Google의 도구 호출 인터페이스로 구조화된 출력을 생성하는 방법을 학습합니다.

2026년 3월 24일·15분
AI / ML

6장: LLM 기반 데이터 추출 자동화

대량 문서 처리 파이프라인을 구축하고, 배치 처리, 비동기 추출, 품질 검증 루프, 비용 최적화 전략을 학습합니다.

2026년 3월 30일·16분
이전 글3장: 함수 호출(Function Calling)과 도구 사용
다음 글5장: 비정형 데이터에서 구조화된 정보 추출

댓글

목차

약 15분 남음
  • 학습 목표
  • Pydantic v2와 JSON Schema
    • 모델 정의
    • JSON Schema 자동 생성
    • 복합 타입과 유니온
  • Instructor 라이브러리
    • 설치 및 기본 사용
    • 자동 재시도
    • 멀티 프로바이더 지원
    • 스트리밍 구조화 출력
  • Pydantic AI의 3가지 출력 방식
    • 1. Tool Output
    • 2. Native Output
    • 3. Prompted Output
  • TypeScript: Zod를 활용한 구조화 출력
  • 정리
  • 다음 장 미리보기