Pydantic를 사용하여 API 및 데이터베이스 경계에서 데이터 무결성 적용
Emily Parker
Product Engineer · Leapcell

소개
현대 소프트웨어 개발에서 데이터는 왕입니다. 데이터의 무결성과 정확성은 애플리케이션의 규모나 복잡성에 관계없이 모든 애플리케이션에 가장 중요합니다. 종종 데이터는 REST API 또는 메시지 큐와 같은 다양한 진입점을 통해 애플리케이션으로 유입된 다음 데이터베이스와 같은 지속성 계층으로 유출됩니다. 이러한 중요한 분기점에서 엄격한 유효성 검사가 없으면 애플리케이션은 잘못된 입력에 취약해지며 오류, 보안 취약점 또는 단순히 부정확한 비즈니스 논리로 이어질 수 있습니다. 이는 손상된 레코드, 예상치 못한 시스템 동작 및 디버깅을 위한 개발 리소스의 상당한 소모로 나타날 수 있습니다. Python은 동적 타이핑으로 유연성을 제공하지만 주의 깊게 관리하지 않으면 데이터 불일치의 가능성도 있습니다. 이것이 Pydantic과 같은 강력한 도구가 등장하는 곳으로, 선언적이고 강력한 방식으로 데이터 스키마를 정의하고 유효성 검사를 시행하여 시스템으로 들어오고 나가는 데이터가 예상 유형 및 구조를 준수하도록 보장합니다.
데이터 유효성 검사에서 Pydantic의 역할
실질적인 적용 사례에 대해 자세히 알아보기 전에 이 논의의 기반이 되는 핵심 개념에 대한 공통된 이해를 확립해 보겠습니다.
Pydantic: Pydantic은 본질적으로 Python 유형 힌트를 사용하여 데이터 유효성 검사 및 설정 관리를 위한 Python 라이브러리입니다. 개발자는 표준 Python 클래스를 사용하여 데이터 모델을 정의할 수 있으며, 여기서 속성은 유형 힌트로 주석이 달립니다. Pydantic은 이러한 유형 힌트에 대해 데이터를 자동으로 유효성을 검사하여 강력한 데이터 구문 분석 및 직렬화를 수행합니다. 들어오는 데이터가 정의된 스키마를 준수하지 않으면 Pydantic은 명확하고 유익한 유효성 검사 오류를 발생시킵니다.
API(애플리케이션 프로그래밍 인터페이스): API는 외부 시스템 또는 클라이언트가 애플리케이션과 상호 작용하기 위한 게이트웨이 역할을 합니다. 웹 애플리케이션의 경우 일반적으로 JSON 형식으로 데이터를 전달하는 들어오는 요청을 수신하는 RESTful 엔드포인트가 포함됩니다. 이 들어오는 데이터의 유효성을 검사하는 것은 잘못된 요청이 애플리케이션의 핵심 논리에 도달하는 것을 방지하는 데 중요합니다.
데이터베이스(지속성 데이터 저장소): 데이터베이스는 애플리케이션이 지속성 데이터를 저장하는 곳입니다. 데이터베이스에 쓸 때(예: ORM 또는 직접 SQL 쿼리를 통해) 이 데이터가 데이터베이스 테이블 또는 컬렉션의 정의된 스키마를 준수하는 것이 중요합니다. 마찬가지로 데이터베이스에서 데이터를 검색할 때 애플리케이션에서 처리하거나 API를 통해 반환하기 전에 구조 및 유형 준수를 보장하는 것이 유익합니다.
데이터 스키마: 데이터 스키마는 데이터의 구조, 유형 및 제약 조건을 정의합니다. Pydantic의 맥락에서 이는 BaseModel 클래스로 표현됩니다. 데이터베이스의 경우 테이블 정의(관계형 데이터베이스의 경우) 또는 문서 구조(NoSQL 데이터베이스의 경우)로 정의됩니다.
API(입력) 및 데이터베이스(출력) 지점 모두에서 Pydantic을 사용하는 원칙은 강력한 데이터 계약을 만드는 것입니다. API에서는 Pydantic이 외부 입력이 이 계약을 준수하는지 확인합니다. 데이터베이스와 상호 작용할 때는 애플리케이션이 이 계약을 일관되게 준수하는 데이터를 처리하고 저장하도록 하여 애플리케이션의 내부 데이터 표현과 지속성 저장소를 일치시킵니다.
Pydantic을 사용한 엄격한 데이터 유효성 검사 구현
API 및 데이터베이스 통합 수준 모두에서 Pydantic을 사용하는 방법을 구체적인 예제를 통해 설명하겠습니다.
들어오는 API 요청 유효성 검사
FastAPI를 사용하여 구축된 간단한 웹 애플리케이션을 고려해 보겠습니다. FastAPI는 본질적으로 요청 본문 유효성 검사를 위해 Pydantic을 활용합니다.
# app/main.py from fastapi import FastAPI, HTTPException from typing import Optional from pydantic import BaseModel, Field app = FastAPI() # 들어오는 사용자 생성 데이터에 대한 Pydantic 모델 class UserCreate(BaseModel): name: str = Field(min_length=2, max_length=50) email: str = Field(pattern=r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") age: Optional[int] = Field(None, gt=0, lt=150) is_active: bool = True # API에서 반환되는 사용자 데이터에 대한 Pydantic 모델 class UserResponse(BaseModel): id: int name: str email: str age: Optional[int] is_active: bool # 데이터베이스 시뮬레이션 fake_db = {} user_id_counter = 0 @app.post("/users/", response_model=UserResponse) async def create_user(user: UserCreate): global user_id_counter user_id_counter += 1 # 실제 앱에서는 데이터베이스 삽입이 이루어짐 new_user_data = user.dict() new_user_data["id"] = user_id_counter fake_db[user_id_counter] = new_user_data return UserResponse(**new_user_data) # 출력이 UserResponse를 준수하도록 보장 @app.get("/users/{user_id}", response_model=UserResponse) async def read_user(user_id: int): user_data = fake_db.get(user_id) if not user_data: raise HTTPException(status_code=404, detail="User not found") return UserResponse(**user_data)
이 예제에서는 다음과 같습니다.
UserCreate는/users/에 대한 들어오는 POST 요청의 예상 스키마를 정의합니다. 요청 본문이 이를 준수하지 않으면(예:name이 너무 짧거나,email이 유효하지 않거나,age가 음수임) FastAPI(Pydantic 사용)는 자세한 유효성 검사 메시지와 함께 422 Unprocessable Entity 오류를 자동으로 반환하여 잘못된 데이터가create_user함수에 도달하는 것을 방지합니다.UserResponse는 API에서 반환되는 데이터의 스키마를 정의합니다. 내부fake_db에 추가 필드가 저장되더라도UserResponse에 정의된 필드만 API 클라이언트에 노출되어 깔끔하고 예측 가능한 API 계약을 유지합니다.
데이터베이스와 상호 작용하는 데이터 유효성 검사
ORM은 일부 수준의 유형 적용을 처리하지만 Pydantic으로 보강하면 데이터베이스에 들어가는 데이터에 대한 강력한 유효성 검사 계층을 제공하고 데이터가 나올 때 일관성을 보장합니다.
SQLAlchemy를 ORM으로 사용하는 시나리오를 생각해 보겠습니다. SQLAlchemy 모델과 일치하는 Pydantic 모델을 정의하고 이를 데이터 변환 및 유효성 검사를 위한 중개자 역할을 하도록 사용할 수 있습니다.
# db_models.py (SQLAlchemy ORM 모델) from sqlalchemy import create_engine, Column, Integer, String, Boolean from sqlalchemy.orm import sessionmaker, declarative_base Base = declarative_base() class DBUser(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) name = Column(String, index=True) email = Column(String, unique=True, index=True) age = Column(Integer, nullable=True) is_active = Column(Boolean, default=True) def __repr__(self): return f"<User(id={self.id}, name='{self.name}', email='{self.email}')>" # 데이터베이스와 일관된 데이터 입출력용 Pydantic 모델 from pydantic import BaseModel, EmailStr, Field from typing import Optional class UserSchema(BaseModel): # 데이터베이스에 들어가는 경우 ID는 자동 생성될 경우 선택 사항일 수 있음 name: str = Field(min_length=2, max_length=50) email: EmailStr # Pydantic의 내장 이메일 유효성 검사 age: Optional[int] = Field(None, gt=0, lt=150) is_active: bool = True class Config: orm_mode = True # Pydantic이 ORM 모델에서 읽을 수 있도록 ORM 모드 활성화 class UserInDB(UserSchema): id: int # DB에서 읽을 때 ID는 필수임 # 서비스 계층 또는 CRUD 작업에서: from sqlalchemy.orm import Session # engine = create_engine("sqlite:///./sql_app.db") # Base.metadata.create_all(bind=engine) # SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def create_user_in_db(db: Session, user: UserSchema): # Pydantic의 UserSchema를 사용하여 입력 데이터 유효성 검사 # user가 인스턴스화될 때 Pydantic의 유효성 검사가 자동으로 이루어짐 db_user = DBUser( name=user.name, email=user.email, age=user.age, is_active=user.is_active ) db.add(db_user) db.commit() db.refresh(db_user) return UserInDB.from_orm(db_user) # 일관된 출력 구조 보장 def get_user_from_db(db: Session, user_id: int): db_user = db.query(DBUser).filter(DBUser.id == user_id).first() if db_user: return UserInDB.from_orm(db_user) # ORM 모델을 Pydantic 모델로 변환하여 일관된 출력 보장 return None
이 설정에서:
- 데이터베이스에 들어가는 데이터(예: 
create_user_in_db호출 시)의 경우UserSchemaPydantic 모델은 함수에 전달된 데이터 객체(user: UserSchema)가 이미 유효성 검사를 통과했음을 보장합니다. 이렇게 하면 ORM에 잘못된 데이터가 전달되는 것을 방지할 수 있으며, ORM은 설명이 부족한 오류로 데이터베이스 수준에서 실패할 수 있습니다. - 데이터베이스에서 나오는 데이터(예: 
get_user_from_db)의 경우DBUser인스턴스를UserInDB.from_orm(db_user)로 변환하면 가져온 데이터가 예상 Pydantic 스키마를 준수하는지 보장합니다. ORM 모델에 많은 필드가 있지만 부분 집합만 사용하거나 API 계층 또는 애플리케이션의 다른 부분에 데이터를 반환하기 전에 특정 유형 변환을 보장해야 하는 경우 특히 유용합니다. Pydantic 모델의orm_mode = True구성은 SQLAlchemy ORM 객체를 직접 수집할 수 있도록 합니다. 
이러한 계층화된 접근 방식은 여러 지점에서 애플리케이션을 보호합니다. API 유효성 검사는 외부 오류를 조기에 포착하고 데이터베이스 상호 작용 유효성 검사는 내부 일관성을 보장하고 지속성 처리를 위한 올바른 데이터 처리를 보장합니다.
결론
API 진입점 및 데이터베이스 상호 작용 계층 모두에서 Pydantic을 전략적으로 배포함으로써 Python 애플리케이션은 강력하고 안정적인 데이터 유효성 검사 전략을 달성할 수 있습니다. 이 접근 방식은 잘못된 요청 및 데이터 손상과 같은 일반적인 문제를 방지할 뿐만 아니라 코드 명확성, 유지 관리 용이성 및 디버깅 용이성을 크게 향상합니다. Pydantic을 채택하면 애플리케이션을 통과하는 데이터가 항상 신뢰할 수 있음을 보장하여 안정적이고 고품질 소프트웨어의 강력한 기반을 형성합니다.