유지보수성 향상, 데이터 검증 및 보안강화를 위해 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
}
🧾 모델과 스키마 분리 장점
- 유지보수성 향상: DB 테이블이 변경되어도 API 스키마를 독립적으로 관리 가능
- 데이터 검증 및 보안 강화: 스키마에서 유효성 검증 수행
- 직관적인 데이터 흐름: 스키마 → 모델 → 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)와의 인터페이스도 명확해집니다.
특히 데이터 유효성 검증, 보안, 유지보수성을 모두 확보할 수 있는 실무적으로 유용한 접근입니다.