[Python] Pydantic으로 안전하고 효율적인 데이터 검증하기
최신 기술 트렌드에 대한 높은 관심과 열정을 가지신 팀장님 덕분에 파이썬에서 Pydantic이라는 유용한 도구를 접하게 되었다. 새롭게 배운 기술에 대해 체계적으로 학습한 내용을 정리해보려고 한다.
Pydantic이란?
Python에서 데이터 유효성 검사(validation)과 데이터 설정(parsing)을 쉽게 처리하기 위한 라이브러리이다. Python의 데이터 클래스(dataclass)와 유사하게 작동하지만, 훨씬 더 강력한 기능을 제공하며, 주로 FastAPI와 같은 웹 프레임워크에서 입력 데이터를 검증하거나 데이터 모델을 정의하는 데 사용된다.
Pydantic의 주요 특징
1. 데이터 유효성 검사: 입력 데이터를 지정된 스키마에 맞게 검증하고, 유효하지 않은 데이터를 처리한다.
예: 숫자 타입이어야 하는 필드에 문자열이 들어오면 자동으로 변환하거나, 오류를 반환
특징 | @validator (Pydantic v1) | @field_validator (Pydantic v2) |
지원되는 버전 | Pydantic v1 | Pydantic v2 |
작동 범위 | 클래스 메서드로 여러 필드에 적용 가능 | 필드 단위로 유효성 검사를 정의 |
필드 처리 방식 | 여러 필드를 동시에 검증 가능 | 주로 개별 필드에 대해 검증 수행 |
옵션 제공 | 제한적 (필드 이름만 지정 가능) | mode 등 정교한 옵센 제공 |
호환성 | Pydantic v1 코드와 호환 | Pydantic v2의 새로운 방식에 최적화 |
설정 가능한 검증 시점 | 불가능 | before, after 등을 지정 가능 |
2. 타입 강제 변환(coercion): 데이터 타입을 지정해두면, Pydantic이 입력 데이터를 자동으로 해당 타입으로 변환한다.
예: "123" 이라는 문자열을 int로 변환
3. Python 타입 힌트 지원: Python의 타이 힌트(typing)를 기반으로 데이터 모델을 정의할 수 있다.
4. JSON 데이터 파싱 및 변환: JSON 데이터 파싱 및 Pyhton 객체로 변환을 손쉽게 처리할 수 있다.
5. FastAPI와의 강력한 통합: Pydantic은 FastAPI에서 요청 및 응답 모델로 사용되며, 자동 문서화(Swagger UI 등)를 제공한다.
6. 읽기 전용(Read-only) 필드 및 기본값: 데이터 모델에서 읽기 전용 필드를 설정하거나 기본값을 지정할 수 있다.
7. 데이터 중첩 및 재사용: 데이터 모델을 중첩하거나, 다른 모델을 재사용할 수 있다.
Pydantic의 장점
- 데이터 무결성 보장: 엄격한 데이터 검증을 통해 예상치 못한 오류를 방지
- 생산성 향상: 데이터 모델링과 검증을 간단하고 빠르게 처리
- 타입 힌트 활용: Python의 정적 타입 시스템과 자연스럽게 통합
Pydantic을 주로 사용 하는 경우
1. API 요청 및 응답 데이터 검증(FastAPI 등에서 사용)
2. 폼 데이터나 JSON 데이터의 파싱 및 검증
3. 데이터 모델링 및 데이터 직렬화/역질렬화
4. 복잡한 데이터 구조의 관리 및 유효성 검사
기본 사용법
1. 기본 모델 정의
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
email: str
# 모델 인스턴스 생성
user = User(id=1, name="YooJaeseok", email="YooJaeseok@naver.com")
print(user)
# 출력 결과
# User(id=1, name='YooJaeseok', email='YooJaeseok@naver.com')
2. 유효성 검사
올바르지 않은 데이터를 넣으면 오류를 반환
try:
invalid_user = User(id="invalid_id", name=123, email="not-an-email")
except Exception as e:
print(e)
# 출력 결과
# 1 validation error for User
# id
# value is not a valid integer (type=type_error.integer)
3. 타입 변환
Pydantic은 가능한 경우 입력 값을 지정된 데이터 타입으로 변환
user = User(id="1", name="YooJaeseok", email="YooJaeseok@naver.com")
print(user)
# 출력 결과
# User(id=1, name='YooJaeseok', email='YooJaeseok@example.com')
# 문자열 "1"이 정수 1로 자동 변환됨.
4. 기본 값과 선택적 필드
from typing import Optional
class User(BaseModel):
id: int
name: str
email: Optional[str] = None # 선택적 필드, 기본값은 None
user = User(id=1, name="Juyoung")
print(user)
# 출력 결과
# User(id=1, name='Juyoung', email=None)
5. 중첩 모델
모델 간의 중첩 사용도 가능
from typing import List
class Address(BaseModel):
city: str
zipcode: str
class User(BaseModel):
id: int
name: str
email: str
addresses: List[Address]
user = User(
id=1,
name="YooJaeseok",
email="YooJaeseok@naver.com",
addresses=[
{"city": "Seoul", "zipcode": "12345"},
{"city": "Busan", "zipcode": "54321"},
]
)
print(user)
# 출력 결과
# User(id=1, name='YooJaeseok', email='YooJaeseok@naver.com',
# addresses=[Address(city='Seoul', zipcode='12345'),
# Address(city='Busan', zipcode='54321')])
6. 유효성 검사 필드(Cutom Validator)
from pydantic import BaseModel, validator
class User(BaseModel):
id: int
name: str
email: str
@validator("email")
def validate_email(cls, value):
if "@" not in value:
raise ValueError("Invalid email address")
return value
# 유효하지 않은 이메일 입력
try:
user = User(id=1, name="YooJaeseok", email="invalid-email")
except Exception as e:
print(e)
# email
# Invalid email address (type=value_error)
고급 기능
1. 스키마 내보내기
print(User.schema_json(indent=4))
# {
# "title": "User",
# "type": "object",
# "properties": {
# "id": {
# "title": "Id",
# "type": "integer"
# },
# "name": {
# "title": "Name",
# "type": "string"
# },
# "email": {
# "title": "Email",
# "type": "string"
# }
# },
# "required": ["id", "name", "email"]
# }
2. ORM 모드
Pydantic은 데이터 베이스 모델(SQLAlchemy 등)과의 호환을 위해 ORM 모드를 지원
from pydantic import BaseModel
class UserOrm(BaseModel):
id: int
name: str
class Config:
orm_mode = True
# SQLAlchemy 객체에서 Pydantic 모델로 변환 가능
db_user = type("DBUser", (), {"id": 1, "name": "YooJaeseok"})
user = UserOrm.from_orm(db_user)
print(user)
# UserOrm(id=1, name='YooJaeseok')
나는 팀원이 생성해 놓은 data_model.py를 사용중이다. 다른 테이블도 더 많지만 이런식으로 사용하고 있다.
SQLAlchemy이 사용되어 있는 SQLModel이다.
타입힌팅을 사용하여 각 필드마다 어떤 타입이어야하는지 명시하고 핸드폰 번호는 Google의 libphonenumber 라이브러리를 import하여 국제 전화번호 형식(E.164)인지 유효성 검증 위해 @field_validator를 추가 했다. 그리고 각 테이블마다 연결되어 있기 때문에 Relationship도 걸어주고 해당 필드나 변수에 값이 없어도 되는 경우에는 Optional까지 설정했다.
from datetime import datetime, time
from typing import List, Optional
import phonenumbers
from pydantic import field_validator
from sqlmodel import Field, Relationship, SQLModel
from sqlalchemy import text
from pydantic import validator
class AdministrativeDivision(SQLModel, table=True):
__tablename__ = "administrative_division"
id: int | None = Field(default=None, primary_key=True)
city_province: str = Field(max_length=50)
city_county: str = Field(max_length=50)
class Member(SQLModel, table=True):
__tablename__ = "member"
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(..., max_length=50, nullable=False) # VARCHAR(50)
email: str = Field(..., max_length=255) # VARCHAR(255)
access_token: str = Field(max_length=2083)
refresh_token: str = Field(max_length=2083)
oauth: str = Field(max_length=50)
nickname: Optional[str] = Field(default=None, max_length=50)
sex: Optional[str] = Field(default=None, max_length=10)
picture_url: Optional[str] = Field(default=None, max_length=2083)
birth: Optional[datetime] = None
address: Optional[str] = Field(default=None, max_length=255)
zip: Optional[str] = Field(default=None, max_length=10)
phone_number: Optional[str] = Field(default=None, max_length=20)
voice: Optional[str] = Field(default=None, max_length=255)
role: Optional[str] = Field(default=None, max_length=10)
created_at: datetime = Field(
sa_column_kwargs={"server_default": text("CURRENT_TIMESTAMP"), "nullable": False}
)
updated_at: datetime = Field(
sa_column_kwargs={"server_default": text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"), "nullable": False}
)
plans: List["Plan"] = Relationship(back_populates="member")
# 전화번호 유효성 검사
@field_validator("phone_number")
def check_phone_number(cls, values):
phone_number = values.get('phone_number')
try:
parsed_number = phonenumbers.is_valid_number(phone_number)
if not parsed_number:
raise ValueError(f"Invalid phone number: {phone_number}")
except phonenumbers.phonenumberutil.NumberParseException as e:
raise ValueError(f"Invalid phone number: {phone_number}") from e
return values
SQLAlchemy이 사용되어 있는 SQLModel에 대해서는 다음글에서 알아보도록 하자.