티스토리 뷰

[FastAPI] 데이터 무결성을 위한 Pydantic 스키마 설계 가이드
FastAPI로 API를 개발할 때 가장 빈번하게 마주치는 과제는 "외부에서 들어오는 데이터가 내가 정의한 구조와 정말 일치하는가?"입니다. 필드가 몇 개 없을 때는 간단하지만, 수십 개의 필드와 복잡한 데이터 타입이 섞이기 시작하면 체계적인 정리가 필요합니다.
1. 필수의 유무: Optional과 Default 처리
가장 먼저 고민해야 할 것은 "이 데이터가 반드시 들어와야 하는가?"입니다.
• 값이 없을 때 (Null/None 처리): Optional (Python 3.10+ 에서는 | None)을 사용하여 해당 필드가 비어있을 수 있음을 명시해야 합니다.
• 기본값 설정: 클라이언트가 값을 보내지 않았을 때 null로 둘지, 아니면 특정 기본값(예: 0, "")을 채울지 정의합니다.
from pydantic import BaseModel, Field
from typing import Optional
class ProductSchema(BaseModel):
name: str # 필수 값
description: Optional[str] = None # 값이 없으면 None (null)
price: float = 0.0 # 값이 없으면 0.0 기본값 사용
is_active: bool = Field(default=True, description="활성화 상태 여부")
엄격한 타입 검증 (정수, 실수, 문자열 등)
Pydantic은 기본적으로 **데이터 타입 변환(Coercion)**을 시도합니다. 예를 들어, 정수 필드에 "123"이라는 문자열이 들어오면 자동으로 123으로 변환합니다. 하지만 더 엄격한 검증이 필요할 때가 있습니다.
• 숫자 범위 제한: gt(greater than), lt(less than), ge(greater or equal) 등을 사용하여 값의 범위를 제한할 수 있습니다.
• 문자열 길이 및 패턴: min_length, max_length 또는 정규표현식(pattern)을 활용합니다.
class UserProfile(BaseModel):
age: int = Field(ge=0, le=120) # 0세~120세 사이 정수만 허용
score: float = Field(default=0.0, gt=-1.0) # -1.0보다 큰 실수
username: str = Field(min_length=2, max_length=20) # 글자수 제한
필드가 많고 복잡할 때: 중첩 모델(Nested Models)
필드가 수십 개가 넘어간다면 하나의 클래스에 모두 정의하는 것은 유지보수에 치명적입니다. 연관된 데이터끼리 그룹화하여 중첩 모델로 설계하세요.
class Address(BaseModel):
city: str
zip_code: str
class UserDetail(BaseModel):
id: int
info: UserProfile # 위에서 정의한 모델 재사용
address: Optional[Address] = None # 중첩된 구조로 가독성 향상
실제 데이터 검토를 위한 Custom Validator
단순히 타입만 맞는다고 끝이 아닙니다. "시작일이 종료일보다 빨라야 한다"와 같은 비즈니스 로직 검증은 @field_validator를 사용합니다.
from pydantic import field_validator
class EventSchema(BaseModel):
start_date: str
end_date: str
@field_validator('end_date')
@classmethod
def check_date_order(cls, v, info):
if 'start_date' in info.data and v < info.data['start_date']:
raise ValueError('종료일은 시작일보다 빨라야 합니다.')
return v
5. 종합적인 설계 전략
많은 필드를 처리해야 하는 대규모 프로젝트라면 다음 전략을 참고하세요.
1. 공통 필드 분리: 생성일, 수정일 등 반복되는 필드는 BaseSchema를 만들어 상속받아 사용합니다.
2. Extra 필드 제어: 정의되지 않은 데이터가 들어오는 것을 막으려면 model_config에서 extra='forbid' 설정을 추가하세요.
3. Alias 사용: DB 필드명과 API 노출 필드명이 다를 경우 Field(alias="...")를 통해 매핑합니다.
마치며
스키마를 정의하는 것은 단순히 에러를 막는 것이 아니라, API의 문서(Swagger)를 만드는 과정이기도 합니다. 처음 설계할 때 Null 처리와 타입 범위를 꼼꼼하게 정의해두면, 실제 데이터가 들어왔을 때 발생하는 예외 상황을 90% 이상 사전 차단할 수 있습니다.
데이터의 구조가 복잡해질수록 "작게 나누고(Composition), 명확하게 제약하는(Validation)" 원칙을 잊지 마세요!