Pydantic은 Python 애플리케이션에서 데이터 유효성 검사를 쉽게 할 수 있도록 돕는 라이브러리이다. 기존의 데이터 검증 도구와 비교했을 때 직관적이고, 성능이 뛰어나며, 유연하게 사용할 수 있다는 점에서 많은 주목을 받고 있다.
왜 이름이 pydantic일까?
웹사이트에 따르면,
Pydantic
이라는 이름은 "Py"와 "pedantic"의 합성어이다. "Py"는 이 라이브러리가 Python과 관련이 있음을 나타내고, "pedantic"(지나치게 규칙을 찾는)은 데이터 검증과 타입 강제에 있어 철저한 접근 방식을 의미한다.이 요소들을 결합하면, "Pydantic"은 세밀하고 엄격한 데이터 검증을 제공하는 Python 라이브러리를 설명한다.
Pydantic의 핵심 개념
Pydantic은 Python 타입 힌팅(Type Hinting)을 적극 활용하여 데이터의 유효성을 검사한다. 주어진 데이터가 유효하지 않으면 명확한 오류 메시지를 반환하며, 이를 통해 데이터 검증이 자연스럽게 이루어진다.
Pydantic의 주요 특징은 다음과 같습니다:
- 타입 유효성 검사(Type Validation): 모델에 정의된 데이터 타입에 따라 자동으로 유효성을 검사한다.
- 자동 형 변환: 전달된 데이터가 정의된 타입과 일치하지 않을 경우 가능한 범위 내에서 자동으로 형 변환을 시도한다.
- 필드의 기본값 지원: 필드에 기본값을 설정하여 사용자가 데이터를 제공하지 않더라도 유효한 모델을 만들 수 있다.
- 복잡한 데이터 구조 지원: Pydantic은 리스트, 딕셔너리, 중첩된 모델과 같은 복잡한 데이터 구조를 지원한다.
Pydantic 모델 생성
Pydantic을 사용하는 기본적인 방법은 BaseModel
클래스를 상속하여 모델을 정의하는 것입니다. 아래 예시는 사용자의 정보를 나타내는 간단한 Pydantic 모델이다.
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
email: str
user_data = {
'id': '123', # 문자열로 제공된 숫자도 자동으로 변환됨
'name': 'John Doe',
'email': 'john.doe@example.com'
}
user = User(**user_data)
print(user)
이 예시에서는 id
필드에 문자열이 전달되었지만, Pydantic은 이를 자동으로 정수로 변환한다. 만약 데이터가 잘못되었다면 명확한 오류 메시지가 반환된다.
데이터 유효성 검사
Pydantic은 기본적인 타입 검사 외에도 정교한 유효성 검사를 지원한다. 예를 들어, 특정 조건을 만족해야 하는 데이터를 검사할 수 있다. 아래 예시는 이메일 형식을 검사하는 방법을 보여준다.
from pydantic import BaseModel, EmailStr
class User(BaseModel):
id: int
name: str
email: EmailStr # 이메일 형식이 아닌 데이터는 오류 발생
try:
user = User(id=1, name='Jane Doe', email='invalid-email')
except ValueError as e:
print(e)
위 코드에서 email
필드에 유효하지 않은 이메일 형식이 입력되면 ValueError
가 발생하고, 사용자에게 잘못된 입력에 대한 피드백을 제공한다.
필드에 대한 세부 설정
Pydantic은 각 필드에 대해 더 세부적인 설정을 할 수 있는 옵션을 제공한다. 예를 들어, 필드가 선택적(Optional)일 경우나, 기본값을 설정하고 싶을 때 다음과 같이 정의할 수 있다.
from typing import Optional
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = 'Anonymous' # 기본값 설정
age: Optional[int] = None # 선택적 필드
user = User(id=2)
print(user)
위 코드에서 name
필드는 기본값을 가지며, age
필드는 선택적이므로 입력하지 않더라도 오류가 발생하지 않는다.
JSON Schema와 예제 정의
Pydantic은 자동으로 JSON Schema를 생성하여 API 문서화를 돕는다. FastAPI와 같은 프레임워크에서 이를 사용하여 간편하게 API 문서를 자동 생성할 수 있다. 또한, 각 필드에 대한 예제를 제공하여 사용자가 데이터를 더 쉽게 이해할 수 있도록 할 수 있다.
from pydantic import BaseModel, Field
class Item(BaseModel):
name: str = Field(..., example="Smartphone")
price: float = Field(..., example=699.99)
print(Item.schema_json(indent=2))
위 코드에서 Field
를 사용하여 필드에 대한 예제를 정의하였으며, 이를 통해 자동 생성된 JSON Schema에는 각 필드의 예제가 포함된다.
커스텀 유효성 검사(Custom Validators)
기본적인 타입 검사 외에도, Pydantic은 각 필드에 대해 사용자 정의 유효성 검사를 추가할 수 있다. 이를 위해 @validator
데코레이터를 사용하여 필드의 값이 특정 조건을 만족하도록 설정할 수 있다.
from pydantic import BaseModel, validator, field_validator
class Product(BaseModel):
name: str
price: float
# 가격이 0보다 커야 한다는 조건 추가
@field_validator("price")
def price_must_be_positive(cls, v):
if v <= 0:
raise ValueError("Price must be greater than 0")
return v
try:
product = Product(name="Laptop", price=-1000)
except ValueError as e:
print(e)
이 예시에서는 price
필드에 대해 커스텀 유효성 검사를 추가하여, 값이 0 이하일 경우 ValueError
를 발생시킵니다.
클래스 전체에 대한 유효성 검사
Pydantic은 필드 단위의 유효성 검사뿐 아니라, 클래스 전체에 대한 유효성 검사를 지원한다. 이를 위해 @root_validator
를 사용한다. 예를 들어, 두 개 이상의 필드가 상호 의존적인 경우, 두 필드를 함께 검증할 수 있다.
from pydantic import BaseModel, root_validator, model_validator
class Order(BaseModel):
item_count: int
total_price: float
# 전체 데이터에 대한 유효성 검사
@model_validator(mode="before")
def check_order(cls, values):
item_count = values.get("item_count")
total_price = values.get("total_price")
if total_price < item_count * 10: # 최소 단가가 10원 이상이어야 함
raise ValueError("Total price is too low for the item count")
return values
try:
order = Order(item_count=5, total_price=40)
except ValueError as e:
print(e)
이 예시에서는 item_count
와 total_price
를 함께 검토하여, 총 가격이 최소 단가를 충족하는지 확인한다.
before
는 pydantic 모델이 파싱되기 전에 필드값 검사를 하고after
는 파싱후에 검사한다.
상속 및 재사용 가능한 모델
Pydantic 모델은 상속을 통해 쉽게 재사용할 수 있다. 기본 모델을 확장하여 새로운 필드를 추가하거나, 기존 필드를 수정할 수 있다.
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
class Admin(User):
access_level: int
admin = Admin(id=1, name="Alice", access_level=5)
print(admin)
이 예시에서는 User
모델을 상속한 Admin
모델을 만들었으며, Admin
모델에는 추가적인 access_level
필드가 있다. 이렇게 하면 기존 모델을 재사용하면서 확장 기능을 구현할 수 있다.
동적 모델 생성
Pydantic은 프로그램 실행 중에 동적으로 모델을 생성할 수 있는 기능을 제공다. 이 기능은 데이터 구조가 동적으로 변할 수 있는 상황에서 매우 유용하다.
from pydantic import create_model
# 동적으로 새로운 모델 생성
DynamicModel = create_model("DynamicModel", foo=(str, ...), bar=(int, 42))
instance = DynamicModel(foo="Hello")
print(instance.model_dump()) # {'foo': 'Hello', 'bar': 42}
위 코드에서는 create_model
을 사용하여 런타임 중에 새로운 Pydantic 모델을 동적으로 생성한다. foo
필드는 필수이며, bar
필드는 기본값을 가진다. 이러한 동적 모델 생성은 매우 복잡한 데이터 구조를 다룰 때 유연하게 사용할 수 있다.
ORM 모드 지원
Pydantic은 데이터베이스와의 연동을 위한 ORM(객체 관계 매핑) 모드도 제공한다. orm_mode
를 활성화하면, Pydantic 모델은 데이터베이스 모델에서 전달된 객체를 처리할 수 있다. 이를 통해 ORM 라이브러리와의 호환성을 높일 수 있다.
from pydantic import BaseModel
class UserORM:
def __init__(self, id: int, name: str):
self.id = id
self.name = name
class User(BaseModel):
id: int
name: str
model_config = {"from_attributes": True}
user_orm = UserORM(id=1, name="Bob")
user = User.model_validate(user_orm)
print(user.model_dump())
이 예시에서는 UserORM
클래스를 정의하고, User
Pydantic 모델이 ORM 객체를 받아들일 수 있도록 orm_mode
를 활성화했다. 이를 통해 ORM 객체를 간단하게 Pydantic 모델로 변환할 수 있다.
컴플렉스 데이터 타입
Pydantic은 기본적인 데이터 타입 외에도, List
, Dict
, Tuple
, Set
등의 복잡한 데이터 타입을 지원한다. 이러한 타입들은 중첩된 구조에서도 유효성 검사를 수행할 수 있다.
from pydantic import BaseModel
from typing import List, Dict
class Item(BaseModel):
name: str
price: float
class ShoppingCart(BaseModel):
items: List[Item]
discounts: Dict[str, float]
cart = ShoppingCart(
items=[Item(name='Laptop', price=1200), Item(name='Mouse', price=25)],
discounts={'SUMMER_SALE': 0.1}
)
print(cart)
위 코드에서 ShoppingCart
는 리스트와 딕셔너리 타입을 가진 필드를 정의하고, Pydantic은 중첩된 Item
모델에 대해서도 유효성 검사를 수행한다.
데이터 변환 및 직렬화
Pydantic은 데이터의 유효성 검사뿐만 아니라, 데이터 변환 및 직렬화에 있어서도 매우 강력하다. 특히, 모델을 Python 객체로 변환하거나 JSON으로 직렬화하는 작업을 매우 쉽게 수행할 수 있다.
데이터 직렬화 및 역직렬화
Pydantic의 모델 인스턴스는 model_dump()
메서드를 사용하여 Python의 기본 자료형으로 변환할 수 있으며, model_dump_json()
메서드를 통해 JSON으로 직렬화할 수도 있다.
from pydantic import BaseModel
from typing import List
class Item(BaseModel):
name: str
price: float
class ShoppingCart(BaseModel):
items: List[Item]
total_price: float
cart = ShoppingCart(
items=[Item(name="Laptop", price=1200), Item(name="Mouse", price=25)],
total_price=1225,
)
# Python 객체로 변환
cart_dict = cart.model_dump()
print(cart_dict)
print(type(cart_dict)) # <class 'dict'>
# JSON 문자열로 변환
cart_json = cart.model_dump_json()
print(cart_json)
print(type(cart_json)) # <class 'str'>
위 코드에서 ShoppingCart
객체는 model_dump()
메서드를 통해 Python의 딕셔너리로 변환될 수 있으며, model_dump_json()
메서드를 사용하여 JSON 형식으로 직렬화할 수 있다. 이를 통해 데이터베이스 저장, 네트워크 전송 등을 쉽게 처리할 수 있다.
데이터 역직렬화
Pydantic은 JSON 문자열이나 다른 포맷에서 역직렬화를 할 수 있는 메서드도 제공한다. 예를 들어, model_validate()
또는 model_validate_json()
메서드를 사용하여 JSON 문자열을 모델 객체로 변환할 수 있다.
cart_dict = {
"items": [{"name": "Laptop", "price": 1200}],
"total_price": 1200,
}
cart_json = '{"items": [{"name": "Laptop", "price": 1200}], "total_price": 1200}'
cart_raw = ShoppingCart.model_validate(cart_dict)
print(cart_raw)
print(type(cart_raw)) # <class '__main__.ShoppingCart'>
cart_raw = ShoppingCart.model_validate_json(cart_json)
print(cart_raw)
print(type(cart_raw)) # <class '__main__.ShoppingCart'>
성능 최적화
Pydantic은 성능 면에서도 매우 최적화되어 있다. 특히 FastAPI와 같은 프레임워크에서 많은 요청을 처리할 때 Pydantic의 성능은 중요한 역할을 한다. 그러나 Pydantic은 더 빠른 성능을 위해 몇 가지 추가 옵션을 제공한다.
validate_assignment
와 use_enum_values
Pydantic 모델에서 데이터의 유효성을 검증할 때, 모든 필드가 다시 검증되지 않도록 설정할 수 있다. 이를 통해 성능을 개선할 수 있다. 특히, 값이 할당될 때마다 유효성 검사를 강제하는 대신, 검증을 생략할 수 있다.
from pydantic import BaseModel
class ConfigModel(BaseModel):
setting_1: str
setting_2: int
model_config = {"validate_assignment": True}
또한, use_enum_values
설정을 통해 enum 값을 보다 빠르게 처리할 수 있습니다.
from enum import Enum
from pydantic import BaseModel
class Status(Enum):
ACTIVE = "active"
INACTIVE = "inactive"
class User(BaseModel):
status: Status
model_config = {
"use_enum_values": True,
}
user = User(status="active")
print(user.status)
pydantic
의 성능 향상 모드
Pydantic은 Cython을 사용한 성능 향상을 지원한다. Cython을 설치한 후, Pydantic
의 성능이 크게 향상될 수 있다. 이 기능은 대규모의 데이터를 처리하는 애플리케이션에서 매우 유용할 수 있다.
pip install "pydantic[email]"
환경 변수로부터 설정 관리
Pydantic은 환경 변수에서 설정을 자동으로 불러오는 기능을 제공하며, 이를 통해 설정 관리가 매우 편리해진다. 특히 BaseSettings
클래스를 사용하면 애플리케이션에서 환경 설정을 쉽게 관리할 수 있다. 이는 설정을 코드에서 분리하여 더 안전하고 유연하게 관리할 수 있도록 돕는다.
예시: 환경 변수에서 설정 불러오기
from pydantic_settings import BaseSettings
class AppConfig(BaseSettings):
app_name: str
admin_email: str
items_per_user: int
model_config = {
"env_file": "../.env"
}
config = AppConfig()
print(config.app_name)
print(config.admin_email)
print(config.items_per_user)
위 코드에서 BaseSettings
는 .env
파일 또는 시스템 환경 변수에서 값을 불러온다. 이를 통해 비밀번호나 API 키 같은 민감한 정보를 코드에 하드코딩하지 않고 환경 변수를 통해 관리할 수 있다.
동적 환경 설정
환경 설정이 복잡할 경우, Pydantic의 유연한 설정 관리를 사용하여 다양한 환경에 맞는 설정을 동적으로 처리할 수 있다. 예를 들어, 개발 환경과 운영 환경에서 설정 값을 다르게 설정할 수 있다.
[.env]
MYAPP_debug=true
MYAPP_database_url=111111111
MYAPP_ secret_key=12345
from pydantic import BaseSettings
class AppConfig(BaseSettings):
debug: bool = False
database_url: str
secret_key: str
model_config = {
"env_prefix": "MYAPP_"
}
config = AppConfig(_env_file='.env')
print(config.debug)
복잡한 데이터 모델 관리
대규모 시스템에서는 여러 계층의 데이터 모델을 관리해야 할 때가 많다. Pydantic은 이를 위해 매우 유연한 구조를 제공한다. 예를 들어, 중첩된 데이터 모델을 사용하여 더욱 복잡한 데이터 구조를 처리할 수 있다.
중첩된 모델
중첩된 데이터 모델을 처리할 때, Pydantic은 자동으로 중첩된 데이터를 유효성 검사하고 처리한다.
from pydantic import BaseModel
from typing import List
class Address(BaseModel):
city: str
postal_code: str
class User(BaseModel):
name: str
address: Address
user_data = {
"name": "John",
"address": {
"city": "New York",
"postal_code": "10001"
}
}
user = User(**user_data)
print(user)
위 예시에서는 User
모델 안에 중첩된 Address
모델을 사용하여 더 복잡한 데이터 구조를 관리한다. 이렇게 중첩된 데이터 구조는 대규모 애플리케이션에서 매우 유용하다.