Pydantic

데이터 검증과 직렬화 – Python 타입 힌트로 스키마를 정의한다

Pydantic은 Python의 타입 힌트를 활용하여 데이터 검증, 직렬화, 역직렬화를 자동화하는 라이브러리이다. BaseModel, Field, validator, JSON 변환, FastAPI와의 결합을 다루고, AI Agent의 요청/응답 계약(contract)을 안전하게 정의하는 방법을 정리한다.

Engineering
저자

Kwangmin Kim

공개

2026년 05월 05일

1 Pydantic의 역할

  • Pydantic은 Python의 타입 힌트를 검증 규칙으로 사용하는 라이브러리이다.
  • API의 요청/응답뿐 아니라 설정 파일 로딩, 데이터 파이프라인의 스키마 정의, 에이전트 간 계약 등 “외부에서 들어오는 데이터의 형식을 보장해야 하는 모든 곳”에서 사용된다.

FastAPI 입문에서 본 기본 패턴을 다시 확인하자:

from pydantic import BaseModel

class RunRequest(BaseModel):
    text: str                          # 필수
    history: list[dict] = []           # 선택 (기본값: 빈 리스트)
    user_id: str | None = None         # 선택 (기본값: None)

request = RunRequest(text="RAG란?")   # 검증 + 변환이 한 줄에 끝난다

타입 힌트 자체가 검증 규칙이 되므로 별도 if 문이 필요 없다. 이 포스트에서는 이 기본 패턴 위에 쌓이는 심화 기능을 하나씩 다룬다.

2 타입별 자동 변환

Pydantic v2는 가능한 경우 자동 타입 변환을 수행한다.

class Config(BaseModel):
    chunk_size: int
    temperature: float
    verbose: bool

# 문자열 → 숫자/불리언 자동 변환
c = Config(chunk_size="1500", temperature="0.7", verbose="true")
print(c.chunk_size)    # 1500 (int)
print(c.temperature)   # 0.7 (float)
print(c.verbose)       # True (bool)

이 자동 변환 덕분에 환경 변수(항상 문자열)나 쿼리 파라미터를 별도 파싱 없이 모델에 바로 넣을 수 있다.

3 Field: 필드 제약 조건

Field로 기본값, 범위, 설명 등 세부 제약을 지정한다.

from pydantic import BaseModel, Field

class RAGConfig(BaseModel):
    chunk_size: int = Field(default=1500, ge=100, le=10000, description="청크 크기 (문자 수)")
    chunk_overlap: int = Field(default=400, ge=0, description="청크 겹침 크기")
    top_k: int = Field(default=5, ge=1, le=50, description="검색 결과 수")
    temperature: float = Field(default=0.7, ge=0, le=2, description="LLM 생성 온도")
    model_name: str = Field(default="gpt-4.1", pattern=r"^gpt-", description="모델 이름")

# 범위 위반
RAGConfig(chunk_size=50)  # ValidationError: ge=100 위반
RAGConfig(temperature=3)  # ValidationError: le=2 위반
RAGConfig(model_name="claude-3")  # ValidationError: pattern 위반
제약 설명 적용 타입
ge, gt 이상, 초과 숫자
le, lt 이하, 미만 숫자
min_length, max_length 문자열/리스트 길이 str, list
pattern 정규식 매칭 str
default 기본값 모든 타입
description OpenAPI 문서에 표시 모든 타입

4 중첩 모델

모델 안에 다른 모델을 포함하여 복잡한 구조를 표현한다.

class Citation(BaseModel):
    source: str
    page: int | None = None
    section: str | None = None

class Response(BaseModel):
    text: str
    citations: list[Citation] = []
    run_id: str
    latency_ms: int
    input_tokens: int = 0
    output_tokens: int = 0

# 중첩 JSON을 자동으로 파싱한다
data = {
    "text": "RAG는 검색 증강 생성이다.",
    "citations": [
        {"source": "guide.pdf", "page": 42, "section": "3.1"},
        {"source": "manual.md"}
    ],
    "run_id": "abc-123",
    "latency_ms": 1500
}

resp = Response(**data)
print(resp.citations[0].source)  # "guide.pdf"
print(resp.citations[1].page)    # None (기본값)

5 JSON 직렬화/역직렬화

# Python 객체 → JSON 문자열
json_str = resp.model_dump_json()
# '{"text":"RAG는...","citations":[...],"run_id":"abc-123",...}'

# Python 객체 → dict
d = resp.model_dump()
# {'text': 'RAG는...', 'citations': [...], 'run_id': 'abc-123', ...}

# JSON 문자열 → Python 객체
resp2 = Response.model_validate_json(json_str)

# dict → Python 객체
resp3 = Response.model_validate(d)
경고

Pydantic v2에서는 .dict().model_dump(), .json().model_dump_json(), .parse_raw().model_validate_json() 으로 메서드명이 변경되었다. v1 메서드는 deprecated이다.

6 Validator: 커스텀 검증 로직

타입과 범위 검증만으로 부족할 때 커스텀 validator를 정의한다.

from pydantic import BaseModel, field_validator

class RunRequest(BaseModel):
    text: str
    history: list[dict] = []
    temperature: float = 0.7

    @field_validator("text")
    @classmethod
    def text_must_not_be_empty(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("빈 문자열은 허용되지 않는다")
        return v.strip()

    @field_validator("history")
    @classmethod
    def validate_history_format(cls, v: list[dict]) -> list[dict]:
        for item in v:
            if "role" not in item or "content" not in item:
                raise ValueError("history 항목에 role과 content가 필요하다")
        return v

RunRequest(text="  ")  # ValidationError: 빈 문자열은 허용되지 않는다
RunRequest(text="질문", history=[{"msg": "hi"}])  # ValidationError: role과 content 필요

7 Enum으로 선택지 제한

from enum import Enum

class AgentMode(str, Enum):
    data = "data"
    code = "code"

class AgentParams(BaseModel):
    mode: AgentMode = AgentMode.data
    stream: bool = False

# 유효한 값만 허용
AgentParams(mode="data")   # OK
AgentParams(mode="code")   # OK
AgentParams(mode="chat")   # ValidationError: 'chat'은 AgentMode에 없다

strEnum을 동시에 상속하면 JSON에서 문자열로 직접 비교할 수 있다.

8 FastAPI와의 결합 요약

FastAPI 입문의 2~3단계에서 다룬 내용을 요약한다. Pydantic 모델은 FastAPI에서 세 가지 역할을 한다.

8.1 요청 모델 (Request Body)

class RunRequest(BaseModel):
    text: str
    history: list[dict] = []
    user_id: str | None = None
    agent_params: AgentParams = AgentParams()

@app.post("/agents/qna_chatbot/run")
def run_agent(request: RunRequest):
    # request는 이미 검증 완료된 Pydantic 객체이다
    return agent.run(request)

8.2 응답 모델 (Response Model)

class RunResponse(BaseModel):
    response: Response
    experiment_id: str | None = None
    arm_id: str | None = None

@app.post("/agents/qna_chatbot/run", response_model=RunResponse)
def run_agent(request: RunRequest):
    result = agent.run(request)
    return RunResponse(response=result)

response_model을 지정하면 FastAPI가 반환값을 해당 모델로 직렬화한다. 모델에 정의되지 않은 필드는 응답에서 자동 제거되어 내부 구현이 노출되지 않는다.

8.3 OpenAPI 스키마 자동 생성

Pydantic 모델의 필드명, 타입, Field의 description이 Swagger UI에 자동으로 표시된다. API 문서를 별도로 작성할 필요가 없다.

9 설정 관리: BaseSettings

환경 변수를 Pydantic으로 관리한다.

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    azure_openai_endpoint: str
    azure_openai_key: str
    llm_provider: str = "azure"
    warmup_on_startup: bool = True
    log_level: str = "INFO"

    model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}

settings = Settings()
# .env 파일 또는 환경변수에서 자동으로 값을 읽어온다

BaseSettingsBaseModel을 상속하므로 동일한 타입 검증이 적용된다. 환경 변수는 항상 문자열이지만 Pydantic이 bool, int 등으로 자동 변환한다.

10 ABC + Pydantic: Agent 계약 패턴

AI Agent 시스템에서 Pydantic은 에이전트 간 계약(contract)을 정의하는 데 사용된다. 입력(Query)과 출력(Response)의 스키마를 Pydantic으로 고정하고, 에이전트 구현은 ABC(Abstract Base Class)로 강제한다.

from abc import ABC, abstractmethod
from pydantic import BaseModel

class Query(BaseModel):
    text: str
    history: list[dict] = []
    user_id: str | None = None

class Response(BaseModel):
    text: str
    citations: list[Citation] = []
    run_id: str

class BaseAgent(ABC):
    name: str

    @abstractmethod
    def run(self, query: Query) -> Response:
        ...

이 패턴의 이점:

  • 새 에이전트를 추가할 때 run(Query) -> Response 인터페이스만 구현하면 된다
  • FastAPI 라우터는 에이전트의 내부 구현을 몰라도 동일한 요청/응답 스키마로 처리할 수 있다
  • Query/Response 스키마가 변경되면 타입 에러가 컴파일 타임에 잡힌다

11 관련 주제

선행 지식

  • API 기초 – JSON, HTTP, REST 개념
  • FastAPI 입문 – 2단계에서 Pydantic 기초(BaseModel, 요청/응답 모델)를 다룬다. 이 포스트는 그 심화편이다

후속 주제 * CORS와 Proxy – 프론트엔드-백엔드 통신의 벽 * SSE – 실시간 스트리밍의 가벼운 선택지 * React 기초 – 컴포넌트, State, Props, Hook * React Router – SPA에서 페이지 전환 * React에서 API 호출 – fetch, 타입 안전 클라이언트 * ASGI와 uvicorn – Python 웹 서버의 구동 원리

다른 카테고리 연결

Subscribe

Enjoy this blog? Get notified of new posts by email: