FastAPI 프로젝트를 운영하다 보면 데이터베이스 모델을 수정해야 하는 경우가 자주 발생합니다.
특히 프로젝트가 커질수록 모델 정의(models.py) 와 실제 테이블 구조(DB Schema) 간의 불일치를 방지하는 것이 중요합니다.
이번 글에서는 자산 앱 프로젝트의 모델링(ERD) 구조를 예시로, Alembic 마이그레이션 도구를 사용해 일부 테이블에만 존재하던 created_at, updated_at 필드를 모델에 추가 정의하고 테이블에 반영하는 방법을 정리해보겠습니다.
💡 문제 상황: 모델은 바뀌었는데 테이블은 그대로인 경우
FastAPI와 SQLModel을 사용하다 보면, 모델 정의를 수정해도 DB 테이블이 자동으로 갱신되지 않는다는 점을 종종 잊기 쉽습니다.
예를 들어 아래처럼 Trade 테이블에만 created_at, updated_at 필드가 있고, 다른 테이블(User, Portfolio, Asset, Deposit)에는 없다고 해봅시다.
class Trade(SQLModel, table=True):
__tablename__ = "trade"
id: Optional[int] = Field(default=None, primary_key=True)
symbol: str
type: TradeType
trade_date: datetime = Field(sa_column=Column(DateTime, nullable=False))
price: int
quantity: float
memo: Optional[str] = None
asset_id: Optional[int] = Field(default=None)
created_at: datetime = Field(
default_factory=lambda: datetime.now(KST),
sa_column=Column(DateTime, nullable=False)
)
updated_at: datetime = Field(
default_factory=lambda: datetime.now(KST),
sa_column=Column(DateTime, onupdate=lambda: datetime.now(KST))
)
이제 이 필드를 모든 테이블(User, Portfolio, Asset, Deposit) 에 추가하고 싶다면, 단순히 모델 파일만 수정하는 것으로는 부족합니다.
왜냐하면 이미 PostgreSQL에 생성된 테이블은 자동으로 변경되지 않기 때문입니다.
⚙️ 모델 수정: 모든 테이블에 created_at / updated_at 추가
모델 정의(models.py)에 다음 두 필드를 공통적으로 추가합니다.
from datetime import datetime, timezone, timedelta
from sqlmodel import SQLModel, Field
from sqlalchemy import Column, DateTime
class BaseModel(SQLModel):
__abstract__ = True
created_at: datetime = Field(default_factory=lambda: datetime.now(KST))
updated_at: datetime = Field(default_factory=lambda: datetime.now(KST))
🔹 코드 의미 상세 설명
__abstract__ = True- 이 클래스를 직접 테이블로 생성하지 않겠다는 의미입니다.
- 즉,
BaseModel자체는 DB에 테이블로 존재하지 않고, 상속받는 모델에서만 컬럼이 추가됩니다.
created_at- 레코드 생성 시각을 저장하는 컬럼입니다.
updated_at- 레코드 수정 시각을 저장하는 컬럼입니다.
✅ 이렇게 정의하면, 상속받는 모든 모델(User, Portfolio, Asset 등)에 자동으로 created_at / updated_at 컬럼이 추가됩니다.
그리고 각 테이블이 이 BaseModel을 상속받게 하면 됩니다.
class User(BaseModel, table=True):
__tablename__ = "users"
id: Optional[int] = Field(default=None, primary_key=True)
email: str
nickname: str
이렇게 하면 모든 테이블에 created_at, updated_at 필드가 공통적으로 들어갑니다.
하지만 여기서 중요한 점은 — 이 변경 사항이 실제 데이터베이스에 자동 반영되지 않는다는 것입니다.
🧩 테이블 변경사항을 반영하는 방법
1. 기존 테이블을 삭제하고 재생성 (개발 환경에서만 권장)
개발 초기라면 가장 간단한 방법은 테이블을 드롭하고 다시 생성하는 것입니다.
# PostgreSQL 접속 후
DROP TABLE users CASCADE;
DROP TABLE portfolio CASCADE;
DROP TABLE asset CASCADE;
DROP TABLE deposit CASCADE;
DROP TABLE trade CASCADE;
그리고 FastAPI 앱을 다시 실행하면 SQLModel이 새 스키마로 테이블을 생성합니다.
python backend/main.py
주의: 이 방법은 운영 환경에서는 절대 사용하지 않습니다. 모든 데이터가 삭제됩니다.
2. Alembic으로 마이그레이션 적용 (운영 환경 권장)
Alembic이란?
Alembic은 SQLAlchemy 기반의 데이터베이스 마이그레이션 도구입니다.
즉, Python 코드로 정의한 모델 변경 사항을 실제 데이터베이스 테이블에 반영하고, 필요할 때 이전 상태로 되돌릴 수 있게 도와주는 도구입니다.
설치
pip install alembic
초기화
alembic init alembic
이 명령을 실행하면 프로젝트 루트에 alembic/ 디렉토리가 생성됩니다.
환경 설정
alembic.ini 파일에서 DB 연결 URL을 수정합니다.
파일은 프로젝트 루트 아래에 생성되어 있습니다.
sqlalchemy.url = postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}
마이그레이션 파일 생성
alembic revision --autogenerate -m "add created_at and updated_at columns"
실제 반영
alembic upgrade head
이렇게 하면 변경된 모델 정의가 실제 PostgreSQL 테이블에 반영됩니다.


🧱 테이블 변경 자동 반영에 대한 오해
가끔 FastAPI나 SQLModel이 “자동으로 DB를 동기화해줄 것”이라 생각하는 경우가 있지만,
SQLModel.create_all() 은 “존재하지 않는 테이블을 생성”할 뿐, 이미 존재하는 테이블을 수정하지는 않습니다.
즉, 필드가 추가되거나 컬럼 타입이 바뀌어도 테이블에는 아무런 변화가 일어나지 않습니다.
이 때문에 Alembic 같은 마이그레이션 도구가 필수적입니다.
🧰 모델과 물리 테이블 관계 정리
| 구분 | 설명 |
|---|---|
| 모델(SQLModel) | Python 객체 구조, ORM 매핑용 |
| 테이블(DB Table) | PostgreSQL 실제 데이터 저장소 |
| 생성 관계 | SQLModel.metadata.create_all(engine) 실행 시 테이블 생성 |
| 변경 관계 | 기존 테이블은 자동 변경되지 않음, Alembic 필요 |
즉, 모델 변경 → Alembic으로 반영 → 실제 DB 변경
이 순서로 진행해야 모델과 DB 간의 불일치 문제를 예방할 수 있습니다.
🧠 추가 팁: Alembic에서 이전 상태로 되돌리는(downgrade) 방법
1️⃣ 현재 상태 확인
먼저, 적용된 마이그레이션 리스트를 확인합니다.
alembic history
- 출력 예시:
7e132e74c6a0 (head) add created_at and updated_at columns
3c2a1b6d9f20 initial migration
head가 현재 최신 마이그레이션입니다.
2️⃣ 특정 버전으로 되돌리기
alembic downgrade <revision_id>
- 예시: 한 단계 이전으로 되돌리기
alembic downgrade -1
- 예시: 특정 리비전으로 되돌리기
alembic downgrade 3c2a1b6d9f20
-1→ 바로 이전 마이그레이션으로 downgrade3c2a1b6d9f20→ 해당 revision ID까지 되돌림
3️⃣ downgrade가 동작하는 이유
마이그레이션 파일에는 upgrade()와 downgrade() 함수가 있습니다.
def upgrade():
op.add_column('asset', sa.Column('created_at', sa.DateTime(), nullable=False))
def downgrade():
op.drop_column('asset', 'created_at')
upgrade()→ 적용할 때 실행downgrade()→ 되돌릴 때 실행
즉, downgrade는 Alembic이 자동으로 이전 상태로 되돌리는 명령이며, 반드시 downgrade() 함수가 정의되어 있어야 합니다.
4️⃣ 주의 사항
- 데이터 손실 가능
- 컬럼을 삭제하는 downgrade는 해당 컬럼 데이터가 모두 삭제됩니다.
- 필요하다면 미리 데이터를 백업하세요.
- 복수 컬럼/테이블 변경 시
- downgrade도 모든 변경 사항을 반영해야 합니다.
- 누락 시 DB 상태가 꼬일 수 있습니다.
- 실제 서비스에서는 주의
- production 환경에서는 직접 컬럼을 삭제하는 것보다,
nullable=True로 바꾸거나, 새 테이블 생성 후 데이터 이전을 고려하는 것이 안전합니다.
- production 환경에서는 직접 컬럼을 삭제하는 것보다,
✅ 정리
| 항목 | 내용 |
|---|---|
| 변경 목적 | 모든 테이블에 created_at, updated_at 추가 |
| 반영 방법 | Alembic 마이그레이션 사용 |
| 주의사항 | create_all()은 기존 테이블을 수정하지 않음 |
| 개발 환경 | FastAPI + SQLModel + PostgreSQL |
| 권장 방식 | BaseModel 상속 + Alembic 자동 생성 및 upgrade |