FastAPI에서 Pydantic 모델 재사용하며 겪은 문제

문제 상황

사용자 관리 API를 만들면서 생성/수정/응답에 같은 필드를 사용하는데, 각각 필수 여부가 달라 모델을 여러 개 만들게 됐다.

class UserCreate(BaseModel):
    email: str
    name: str
    password: str

class UserUpdate(BaseModel):
    email: Optional[str]
    name: Optional[str]
    password: Optional[str]

class UserResponse(BaseModel):
    id: int
    email: str
    name: str

필드가 늘어날수록 중복 코드가 많아지고, 검증 로직도 따로 관리해야 했다.

해결 방법

Base 모델을 만들고 상속 구조로 정리했다.

class UserBase(BaseModel):
    email: EmailStr
    name: str

class UserCreate(UserBase):
    password: str

class UserUpdate(BaseModel):
    email: Optional[EmailStr] = None
    name: Optional[str] = None
    password: Optional[str] = None

class UserResponse(UserBase):
    id: int
    created_at: datetime

    class Config:
        orm_mode = True

Update는 모든 필드가 선택적이어서 Base를 상속받지 않았다. PATCH 요청 시 제공된 필드만 업데이트하려면 None과 "필드 없음"을 구분해야 해서 초기엔 까다로웠다.

@router.patch("/users/{user_id}")
async def update_user(user_id: int, user: UserUpdate):
    update_data = user.dict(exclude_unset=True)
    # exclude_unset=True로 실제 전달된 필드만 dict에 포함
    return db.update(user_id, update_data)

배운 점

  • Pydantic의 exclude_unset 옵션으로 PATCH 의도를 명확히 표현할 수 있었다
  • EmailStr 같은 내장 validator를 적극 활용하니 별도 검증 코드가 줄었다
  • ORM 모델과 Pydantic 모델을 분리하니 계층이 명확해졌다
FastAPI에서 Pydantic 모델 재사용하며 겪은 문제