FastAPI에서 모델(Model)과 스키마(Schema)의 분리

유지보수성 향상, 데이터 검증 및 보안강화를 위해 FastAPI와 SQLModel을 함께 사용하면 데이터베이스 테이블 정의(모델)와 API 요청/응답 검증(스키마)을 분리할 수 있습니다.
이번 포스팅에서는 FastAPI + SQLModel + PostgreSQL(Docker) 환경을 기반으로, 스키마(Schema)에서 받은 데이터를 안전하게 모델(Model)로 변환하여 저장하는 방법을 구체적인 코드 예시와 함께 살펴보겠습니다.


🧭 개발 환경

  • OS: Windows
  • IDE: VS Code
  • 형상관리: Git
  • FE: Next.js (v15.5.4)
  • BE: FastAPI (v0.118.2)
  • DB: PostgreSQL (v15.14, Docker 기반)
  • 기타 패키지
    • mantine@8.3.5
    • axios@1.12.2
    • next-auth@4.24.11
    • typescript@5.9.3

📂 프로젝트 구조

PORTFOLIO-APP/
├── backend/
│   ├── models/
│   │   └── portfolio_model.py
│   ├── routes/
│   │   └── portfolio.py
│   ├── schemas/
│   │   └── portfolio_schema.py
│   ├── db.py
│   ├── main.py
│   └── requirements.txt
│
├── frontend/
│   ├── pages/
│   ├── components/
│   ├── lib/
│   ├── styles/
│   ├── package.json
│   └── tsconfig.json
│
├── docker-compose.yml
├── package-lock.json
└── package.json

위 구조는 FastAPI 백엔드에서 모델과 스키마를 완전히 분리하는 대표적인 패턴입니다.
models/ 폴더는 DB ORM 정의, schemas/ 폴더는 요청/응답 데이터 구조 정의를 담당합니다.


🧱 모델 정의 (models/portfolio_model.py)

SQLModel을 사용하여 DB 모델을 정의합니다.
Portfolio 테이블은 사용자(User)와 관계를 가지며, 포트폴리오 이름과 자산 목록을 포함합니다.

from typing import Optional, List
from sqlmodel import SQLModel, Field, Relationship
from backend.models.user_model import User  # 사용자 모델

class Portfolio(SQLModel, table=True):
    __tablename__ = "portfolio"
    __table_args__ = {'extend_existing': True}

    id: Optional[int] = Field(default=None, primary_key=True)
    name: str  # 포트폴리오명
    assets: List["Asset"] = Relationship(back_populates="portfolio")
    user_id: Optional[int] = Field(default=None, foreign_key="users.id")  # 🔑 ForeignKey 추가
    user: Optional[User] = Relationship(back_populates="portfolios")

💡 설명

  • Portfolio 클래스는 SQLModel 기반 ORM으로 DB의 portfolio 테이블과 매핑됩니다.
  • Relationship()은 다른 테이블(예: Asset, User)과의 연관 관계를 정의합니다.
  • user_id는 외래 키(users.id)를 명시하여 사용자 테이블과 연결합니다.

📜 스키마 정의 (schemas/portfolio_schema.py)

API 요청 및 응답을 담당하는 Pydantic 스키마를 정의합니다.

from typing import Optional
from sqlmodel import SQLModel

class PortfolioBase(SQLModel):
    name: str
    user_id: Optional[int] = None

class PortfolioCreate(PortfolioBase):
    pass

class PortfolioUpdate(SQLModel):
    name: Optional[str] = None

class PortfolioRead(PortfolioBase):
    id: int

💡 스키마 구조 요약

  • PortfolioBase: 공통 필드(name, user_id)
  • PortfolioCreate: 생성용 스키마 (POST 요청)
  • PortfolioUpdate: 수정용 스키마 (PATCH/PUT 요청)
  • PortfolioRead: 응답용 스키마 (GET 응답)


🔄 스키마 → 모델 변환 (routes/portfolio.py)

스키마에서 모델로 변환하여 DB에 저장하는 핵심 로직입니다.

from fastapi import APIRouter, Depends
from sqlmodel import Session
from backend.db import get_session
from backend.models.portfolio_model import Portfolio
from backend.schemas.portfolio_schema import PortfolioCreate, PortfolioRead

router = APIRouter(prefix="/portfolio", tags=["portfolio"])

# ------------------------------
# Create
# ------------------------------
@router.post("/", response_model=PortfolioRead)
def create_portfolio(portfolio: PortfolioCreate, session: Session = Depends(get_session)):
    # ✅ 스키마 → 모델 변환
    db_portfolio = Portfolio.model_validate(portfolio)

    session.add(db_portfolio)
    session.commit()
    session.refresh(db_portfolio)
    return db_portfolio

💡 핵심 포인트

항목설명
Portfolio.model_validate(portfolio)SQLModel이 제공하는 유틸리티로, Pydantic 스키마 인스턴스를 모델 객체로 변환
session.add()DB 세션에 객체 추가
session.commit()트랜잭션 커밋
session.refresh()커밋 후 새로 생성된 PK(id) 등 반영

🚀 main.py에 라우터 등록

from fastapi import FastAPI
from backend.routes import portfolio
from sqlmodel import SQLModel
from backend.db import engine

app = FastAPI(title="Portfolio API")

# 테이블 생성
SQLModel.metadata.create_all(engine)

# 라우터 등록
app.include_router(portfolio.router)

🧪 실행 및 테스트

1️⃣ 서버 실행

uvicorn backend.main:app --reload

2️⃣ Swagger 접속

브라우저에서 http://localhost:8000/docs 접속 → /portfolio/ POST 요청에 다음 JSON 입력:

{
  "name": "My First Portfolio",
  "user_id": 1
}

응답 예시:

{
  "id": 1,
  "name": "My First Portfolio",
  "user_id": 1
}

🧾 모델과 스키마 분리 장점

  1. 유지보수성 향상: DB 테이블이 변경되어도 API 스키마를 독립적으로 관리 가능
  2. 데이터 검증 및 보안 강화: 스키마에서 유효성 검증 수행
  3. 직관적인 데이터 흐름: 스키마 → 모델 → DB → 응답 스키마 구조로 명확한 데이터 파이프라인 구성

🧠 추가 팁

  • 수정 API에서는 PortfolioUpdate 스키마를 활용해 exclude_unset=True 옵션으로 부분 업데이트 가능:
data = portfolio_update.dict(exclude_unset=True)
for key, value in data.items():
    setattr(db_portfolio, key, value)
  • 관계형 모델(Relationship)을 사용할 경우, 응답 스키마에 중첩 구조를 명시하면 직관적인 JSON 응답 구성 가능

📘 마무리

이번 글에서는 FastAPI에서 모델과 스키마를 분리하고
model_validate()를 활용해 스키마 → 모델 변환을 안전하게 처리하는 방법을 다뤘습니다.

이 패턴을 적용하면 API 구조가 단단해지고, 프론트엔드(Next.js)와의 인터페이스도 명확해집니다.
특히 데이터 유효성 검증, 보안, 유지보수성을 모두 확보할 수 있는 실무적으로 유용한 접근입니다.


🔗 참고

댓글 남기기