Pydantic 모델을 사용한 API 계층 분리로 견고한 데이터 전송
Min-jun Kim
Dev Intern · Leapcell

소개
현대 소프트웨어 개발의 복잡한 세계에서 견고하고 유지보수하기 쉬운 API를 구축하는 것은 매우 중요합니다. 애플리케이션이 복잡해짐에 따라 다양한 계층 간의 데이터 흐름을 관리하는 문제는 더욱 심화됩니다. 흔히 저지르는 실수는 API의 데이터 계약을 데이터베이스의 ORM(객체-관계 매핑) 모델에 직접적으로 타이트하게 결합하는 것입니다. 처음에는 편리해 보일 수 있지만, 이 접근 방식은 보안 취약성, 유연성 감소, 유지보수 오버헤드 증가를 포함한 수많은 문제로 이어지는 경우가 많습니다. 이 글에서는 Pydantic 모델을 Python API에서 강력한 DTO(Data Transfer Object)로 활용하여 API 계층을 ORM 모델과 분리하는 명확하고 우아한 솔루션을 제공하는 방법에 대해 자세히 알아볼 것입니다. 이 패턴을 채택함으로써 개발자는 데이터 노출에 대한 통제력을 강화하고, 검증을 개선하며, 보다 복원력 있고 확장 가능한 애플리케이션을 위한 길을 열 수 있습니다.
핵심 개념 이해
실제 적용에 들어가기 전에, 논의에 중요한 몇 가지 기본 용어를 명확히 하겠습니다.
- API (Application Programming Interface): 서로 다른 소프트웨어 애플리케이션이 서로 통신할 수 있도록 허용하는 정의된 규칙 집합입니다. 웹 환경에서는 일반적으로 클라이언트(예: 웹 브라우저, 모바일 앱)가 서버의 리소스와 상호 작용하는 방식을 정의합니다.
- ORM (Object-Relational Mapping): 객체 지향 프로그래밍 언어와 관계형 데이터베이스와 같이 호환되지 않는 유형 시스템 간의 데이터를 변환하는 프로그래밍 기법입니다. ORM을 통해 개발자는 원시 SQL 쿼리 대신 객체와 메서드를 사용하여 데이터베이스와 상호 작용할 수 있습니다. 예로는 SQLAlchemy, Django ORM, Peewee 등이 있습니다.
- DTO (Data Transfer Object): 프로세스 간에 데이터를 전달하는 객체입니다. 주요 목적은 종종 애플리케이션의 서로 다른 아키텍처 계층 간에 데이터를 캡슐화하여 전송하는 것입니다. DTO는 일반적으로 비즈니스 로직을 포함하지 않으며 데이터 표현에만 초점을 맞춥니다.
- Pydantic: Python 유형 힌트를 사용하여 데이터 검증 및 설정 관리를 위한 Python 라이브러리입니다. Pydantic을 사용하면 Python 클래스로 데이터 스키마를 정의할 수 있으며, 즉시 사용 가능한 강력한 검증, 직렬화 및 역직렬화 기능을 제공합니다.
우리가 해결하려는 핵심 문제는 데이터베이스 상호 작용을 위해 설계된 ORM 모델이 API의 데이터 계약으로 직접 노출될 때 발생합니다.
- 데이터 과다 노출: ORM 모델에는 종종 공개 API에 노출되어서는 안 되는 민감한 필드 또는 내부 데이터베이스별 세부 정보가 포함됩니다.
- 타이트한 결합: 데이터베이스 스키마의 변경 사항은 API 요구 사항이 변경되지 않았더라도 API 계약에 직접적인 영향을 미칩니다. 이는 데이터베이스와 API의 독립적인 진화를 어렵게 만듭니다.
- 입력 유효성 검사 부족: ORM 모델은 주로 데이터 지속성에 초점을 맞추며 API 요청에 대한 강력한 입력 유효성 검사에는 그렇지 않습니다.
- 보안 문제: ORM 모델을 직접 노출하면 신중하게 관리하지 않으면 대량 할당 취약점에 대한 문이 열릴 수 있습니다.
DTO로서 Pydantic의 힘
Pydantic 모델은 API 요청 및 응답에 대한 명확하고 명시적인 데이터 스키마를 정의할 수 있게 해주므로 DTO로서 돋보입니다. 이러한 스키마는 ORM 모델과 독립적이므로 원하는 분리를 제공합니다.
Pydantic이 분리하는 방법
원리는 간단합니다.
- API 입력 (요청 본문): 클라이언트가 API에 데이터를 보내면 API의 입력 요구 사항을 위해 특별히 설계된 Pydantic 모델을 사용하여 이를 검증하고 구문 분석합니다. 이 모델은 API가 예상하는 데이터를 정확히 정의하고 그 정확성을 보장합니다.
- API 출력 (응답 본문): 클라이언트에 데이터를 다시 보내기 전에 ORM 모델 인스턴스를 API 출력에 최적화된 Pydantic 모델로 변환합니다. 이를 통해 노출할 필드를 선택하거나, 필드 이름을 바꾸거나, 필요한 형식을 적용할 수 있습니다.
코드 예제를 통한 실제 구현
User 엔티티가 관련된 간단한 시나리오를 통해 이를 설명해 보겠습니다.
먼저 ORM 모델이 있다고 가정해 보겠습니다 (이 예에서는 SQLAlchemy를 사용하지만 원칙은 모든 ORM에 적용됩니다).
# orm_models.py from sqlalchemy import Column, Integer, String, Boolean from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) username = Column(String, unique=True, index=True) email = Column(String, unique=True, index=True) hashed_password = Column(String) # API를 통해 직접 노출되어서는 안 됩니다 is_active = Column(Boolean, default=True) created_at = Column(String) # 데이터베이스 특정 필드의 예
이제 입력 및 출력에 대한 Pydantic DTO를 정의해 보겠습니다.
# schemas.py from pydantic import BaseModel, Field, EmailStr from typing import Optional # 새 사용자 생성 (입력)을 위한 Pydantic DTO class UserCreate(BaseModel): username: str = Field(..., min_length=3, max_length=50) email: EmailStr password: str = Field(..., min_length=8) # 'id', 'hashed_password', 'is_active', 'created_at'를 입력으로 포함하지 않는 점에 유의하세요 # 기존 사용자 업데이트 (입력)를 위한 Pydantic DTO class UserUpdate(BaseModel): username: Optional[str] = Field(None, min_length=3, max_length=50) email: Optional[EmailStr] = None is_active: Optional[bool] = None # 사용자 데이터 읽기 (출력)를 위한 Pydantic DTO class UserInDB(BaseModel): id: int username: str email: EmailStr is_active: bool # 'hashed_password'와 'created_at'를 API 응답에서 명시적으로 제외합니다. class Config: orm_mode = True # Pydantic이 ORM 모델에서 직접 읽을 수 있도록 합니다. # 필드 이름이 다르거나 유형을 조정해야 하는 경우에도 가능합니다.
다음으로, API 엔드포인트에서 이러한 DTO가 어떻게 사용되는지 살펴보겠습니다 (간결성을 위해 FastAPI를 사용하지만 개념은 이전 가능합니다).
# main.py (API 엔드포인트 예제) from fastapi import FastAPI, Depends, HTTPException, status from sqlalchemy.orm import Session from . import orm_models, schemas # 이들이 동일한 패키지에 있다고 가정 from .database import engine, get_db # 데이터베이스 설정 # 데이터베이스 테이블 생성 orm_models.Base.metadata.create_all(bind=engine) app = FastAPI() # 데이터베이스 세션 가져오기 의존성 def get_db_session(): db = get_db() try: yield db finally: db.close() @app.post("/users/", response_model=schemas.UserInDB, status_code=status.HTTP_201_CREATED) async def create_user(user: schemas.UserCreate, db: Session = Depends(get_db_session)): # 1. Pydantic DTO를 사용한 입력 유효성 검사 (FastAPI는 자동으로 이를 수행합니다) # 'user' 객체는 이미 UserCreate 스키마에 따라 유효하게 검증되었습니다. # 2. 비밀번호 해싱 (DTO에 포함되지 않은 비즈니스 로직) hashed_password = f"super_secure_hash_of_{user.password}" # 실제 해싱으로 대체 # 3. 유효하게 검증된 DTO 데이터에서 ORM 모델 인스턴스 생성 db_user = orm_models.User( username=user.username, email=user.email, hashed_password=hashed_password, is_active=True, # 기본값, 필요한 경우 DTO에 포함될 수도 있습니다. created_at="2023-10-27" # 예: 데이터베이스가 이를 처리합니다. ) db.add(db_user) db.commit() db.refresh(db_user) # 자동 생성된 ID를 얻기 위해 새로고침 # 4. 응답을 위한 Pydantic DTO로 ORM 모델 다시 변환 # Pydantic의 `orm_mode = True`는 이를 원활하게 만듭니다. return db_user @app.get("/users/{user_id}", response_model=schemas.UserInDB) async def read_user(user_id: int, db: Session = Depends(get_db_session)): db_user = db.query(orm_models.User).filter(orm_models.User.id == user_id).first() if db_user is None: raise HTTPException(status_code=404, detail="User not found") # Pydantic의 `orm_mode = True`가 응답에 대한 변환을 처리합니다. return db_user @app.put("/users/{user_id}", response_model=schemas.UserInDB) async def update_user(user_id: int, user_update: schemas.UserUpdate, db: Session = Depends(get_db_session)): db_user = db.query(orm_models.User).filter(orm_models.User.id == user_id).first() if db_user is None: raise HTTPException(status_code=404, detail="User not found") # Pydantic DTO의 데이터에서 ORM 모델 업데이트 update_data = user_update.dict(exclude_unset=True) # 실제로 제공된 필드만 가져옵니다. for key, value in update_data.items(): setattr(db_user, key, value) db.add(db_user) db.commit() db.refresh(db_user) return db_user
이점 및 적용
Pydantic DTO를 사용함으로써 우리는 다음을 달성합니다.
- 명확한 관심사 분리:
UserORM 모델은 데이터베이스 지속성에 초점을 맞추고,UserCreate,UserUpdate,UserInDB는 API 데이터 계약에 초점을 맞춥니다. - 향상된 보안:
hashed_password또는 내부created_at타임스탬프와 같은 내부 ORM 필드를 API에 직접 노출하지 마십시오. DTO는 무엇이 들어가고 나가는지에 대한 허용 목록 역할을 합니다. - 강력한 데이터 유효성 검사: Pydantic은 정의된 스키마에 대해 들어오는 요청 본문을 자동으로 유효성을 검사합니다. 여기에는 유형 검사, 필드 제약 조건 (min_length, max_length) 및 사용자 지정 유효성 검사기가 포함됩니다.
- 개선된 유지보수성: (예: 새 ORM 필드 추가) 데이터베이스 스키마의 변경 사항은 DTO가 안정적인 한 API를 즉시 중단시키지 않습니다. 반대로 API 요구 사항의 변경 사항 (예: 선택적 필드 추가)은 관련 DTO에만 영향을 미칩니다.
- 가독성 향상: API 계약은 Pydantic 모델을 통해 명확하게 정의되고 자체 문서화됩니다.
- 유연성: ORM 모델과 DTO 간에 데이터를 쉽게 변환하여 API 소비자의 특정 요구 사항에 맞게 데이터를 조정할 수 있습니다. 예를 들어, 관리자 API와 공개 API에 대해 서로 다른
UserInDB모델을 가질 수 있습니다. - 자동 문서화: FastAPI와 같은 프레임워크는 Pydantic 스키마에서 직접 OpenAPI (Swagger) 문서를 자동으로 생성하여 정확하고 최신 상태의 API 사양을 제공할 수 있습니다.
이 패턴은 간단한 마이크로서비스부터 복잡한 엔터프라이즈 시스템에 이르기까지 다양한 애플리케이션에 적용되어 일관된 데이터 처리와 깔끔한 아키텍처 분리를 보장합니다.
결론
데이터 전송 객체로서 Pydantic 모델의 현명한 사용은 Python API 구축을 위한 강력한 아키텍처 패턴입니다. 애플리케이션의 API 계층과 ORM 모델 사이에 명확한 경계를 수립함으로써 보안, 유효성 검사, 유연성 및 유지보수성 측면에서 상당한 이점을 얻을 수 있습니다. 이러한 분리는 API 계약이 명확하고 안전하며 독립적이도록 보장하여 보다 강력하고 확장 가능한 소프트웨어 솔루션으로 이어집니다. 궁극적으로 Pydantic은 개발자가 성능이 뛰어날 뿐만 아니라 구축하고 진화시키는 데 즐거움을 주는 API를 만들 수 있도록 지원합니다.