Alert

์ด ๊ธ€์€ Claude Code์˜ ๋„์›€์„ ๋ฐ›์•„ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค

TL;DR

  • Alembic์€ SQLAlchemy ์ „์šฉ DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋„๊ตฌ
  • ๋ชจ๋ธ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ž๋™ ๊ฐ์ง€ํ•ด์„œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ ์ƒ์„ฑ ๊ฐ€๋Šฅ
  • upgrade/downgrade๋กœ ์Šคํ‚ค๋งˆ ๋ฒ„์ „ ๊ด€๋ฆฌ ๋ฐ ๋กค๋ฐฑ ์ง€์›
  • FastAPI + SQLAlchemy ์กฐํ•ฉ์—์„œ ์‚ฌ์‹ค์ƒ ํ‘œ์ค€

Alembic?

  • SQLAlchemy ํ”„๋กœ์ ํŠธ์˜ DB ์Šคํ‚ค๋งˆ๋ฅผ ๋ฒ„์ „ ๊ด€๋ฆฌํ•˜๋Š” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋„๊ตฌ
  • Django์˜ makemigrations/migrate์™€ ๊ฐ™์€ ์—ญํ• 
  • SQLAlchemy ์ œ์ž‘์ž(Mike Bayer)๊ฐ€ ์ง์ ‘ ๊ฐœ๋ฐœ
  • https://alembic.sqlalchemy.org/

1. ์™œ ํ•„์š”ํ•œ๊ฐ€

DB ์Šคํ‚ค๋งˆ๋Š” ๊ฐœ๋ฐœ ๊ณผ์ •์—์„œ ๊ณ„์† ๋ฐ”๋€๋‹ค. ํ…Œ์ด๋ธ” ์ถ”๊ฐ€, ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ, ์ธ๋ฑ์Šค ์ƒ์„ฑ ๋“ฑ์˜ ๋ณ€๊ฒฝ์„ ์ˆ˜๋™์œผ๋กœ SQL์„ ์‹คํ–‰ํ•ด์„œ ๊ด€๋ฆฌํ•˜๋ฉด ๋‹ค์Œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธด๋‹ค.

  • ํŒ€์› ๊ฐ„ ์Šคํ‚ค๋งˆ ๋ถˆ์ผ์น˜
  • ์–ด๋–ค ๋ณ€๊ฒฝ์ด ์–ธ์ œ ์ ์šฉ๋˜์—ˆ๋Š”์ง€ ์ถ”์  ๋ถˆ๊ฐ€
  • ๋กค๋ฐฑ์ด ์–ด๋ ค์›€
  • ๋กœ์ปฌ/์Šคํ…Œ์ด์ง•/ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ ๊ฐ„ ์Šคํ‚ค๋งˆ ์ฐจ์ด

Alembic์€ ์ด๋Ÿฐ ๋ณ€๊ฒฝ์„ Python ์Šคํฌ๋ฆฝํŠธ๋กœ ๊ด€๋ฆฌํ•˜๊ณ , git์ฒ˜๋Ÿผ ๋ฒ„์ „์„ ์ถ”์ ํ•œ๋‹ค.


2. ์„ค์น˜

pip install alembic

SQLAlchemy๊ฐ€ ํ•จ๊ป˜ ์„ค์น˜๋œ๋‹ค. uv๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ:

uv add alembic

3. ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐํ™”

ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์—์„œ alembic init ๋ช…๋ น์œผ๋กœ ์ดˆ๊ธฐํ™”ํ•œ๋‹ค.

alembic init alembic

์•„๋ž˜์™€ ๊ฐ™์€ ๊ตฌ์กฐ๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค.

project/
โ”œโ”€โ”€ alembic.ini            # ์„ค์ • ํŒŒ์ผ (DB ์—ฐ๊ฒฐ ์ •๋ณด ๋“ฑ)
โ”œโ”€โ”€ alembic/
โ”‚   โ”œโ”€โ”€ env.py             # ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ํ™˜๊ฒฝ ์„ค์ •
โ”‚   โ”œโ”€โ”€ script.py.mako     # ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ํ…œํ”Œ๋ฆฟ
โ”‚   โ””โ”€โ”€ versions/          # ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ €์žฅ ๋””๋ ‰ํ† ๋ฆฌ
โ””โ”€โ”€ models.py              # SQLAlchemy ๋ชจ๋ธ (์ง์ ‘ ์ž‘์„ฑ)
alembic.ini ์„ค์ •

DB ์—ฐ๊ฒฐ ๋ฌธ์ž์—ด์„ ์„ค์ •ํ•œ๋‹ค.

# alembic.ini
sqlalchemy.url = postgresql://user:password@localhost/mydb

ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์‚ฌ์šฉ

DB ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ini ํŒŒ์ผ์— ์ง์ ‘ ๋„ฃ์ง€ ๋ง๊ณ , env.py์—์„œ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ฃผ์ž…ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•œ๋‹ค.

# env.py
import os
config.set_main_option(
    "sqlalchemy.url",
    os.environ["DATABASE_URL"]
)

4. ๋ชจ๋ธ๊ณผ ์—ฐ๋™ (autogenerate ์„ค์ •)

Alembic์ด SQLAlchemy ๋ชจ๋ธ ๋ณ€๊ฒฝ์„ ์ž๋™ ๊ฐ์ง€ํ•˜๋ ค๋ฉด env.py์—์„œ ๋ชจ๋ธ์˜ MetaData๋ฅผ ์—ฐ๊ฒฐํ•ด์•ผ ํ•œ๋‹ค.

# models.py
from sqlalchemy.orm import DeclarativeBase
 
class Base(DeclarativeBase):
    pass
 
class User(Base):
    __tablename__ = "users"
 
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(100), nullable=False)
    email = mapped_column(String(200), unique=True)
# alembic/env.py
from models import Base
 
target_metadata = Base.metadata

์ด ์„ค์ •์ด ๋๋‚˜๋ฉด --autogenerate ์˜ต์…˜์œผ๋กœ ๋ชจ๋ธ๊ณผ DB์˜ ์ฐจ์ด๋ฅผ ์ž๋™ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.


5. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ

์ž๋™ ์ƒ์„ฑ (autogenerate)

๋ชจ๋ธ ์ฝ”๋“œ์™€ ์‹ค์ œ DB๋ฅผ ๋น„๊ตํ•ด์„œ ์ฐจ์ด์ ์„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ๋กœ ๋งŒ๋“ ๋‹ค.

alembic revision --autogenerate -m "add users table"
์ˆ˜๋™ ์ƒ์„ฑ

๋นˆ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ  ์ง์ ‘ ๋‚ด์šฉ์„ ์ž‘์„ฑํ•œ๋‹ค.

alembic revision -m "add custom index"
์ƒ์„ฑ๋œ ํŒŒ์ผ ์˜ˆ์‹œ

versions/ ๋””๋ ‰ํ† ๋ฆฌ์— ์•„๋ž˜์™€ ๊ฐ™์€ ํŒŒ์ผ์ด ์ƒ์„ฑ๋œ๋‹ค.

"""add users table"""
 
# revision identifiers
revision = 'a1b2c3d4e5f6'
down_revision = None  # ์ฒซ ๋ฒˆ์งธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด๋ฉด None
 
from alembic import op
import sqlalchemy as sa
 
def upgrade():
    op.create_table(
        'users',
        sa.Column('id', sa.Integer, primary_key=True),
        sa.Column('name', sa.String(100), nullable=False),
        sa.Column('email', sa.String(200), unique=True),
    )
 
def downgrade():
    op.drop_table('users')
  • upgrade(): ์Šคํ‚ค๋งˆ๋ฅผ ์•ž์œผ๋กœ ์ ์šฉํ•˜๋Š” ๋กœ์ง
  • downgrade(): ๋กค๋ฐฑ ๋กœ์ง
  • down_revision: ์ด์ „ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ๊ฐ€๋ฆฌํ‚ค๋Š” ํฌ์ธํ„ฐ (linked list ๊ตฌ์กฐ)

6. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ์šฉ

# ์ตœ์‹  ๋ฒ„์ „์œผ๋กœ ์ ์šฉ
alembic upgrade head
 
# ํŠน์ • ๋ฆฌ๋น„์ „์œผ๋กœ ์ ์šฉ
alembic upgrade a1b2c3d4e5f6
 
# ํ•œ ๋‹จ๊ณ„๋งŒ ์ ์šฉ
alembic upgrade +1

7. ๋กค๋ฐฑ

# ํ•œ ๋‹จ๊ณ„ ๋กค๋ฐฑ
alembic downgrade -1
 
# ํŠน์ • ๋ฆฌ๋น„์ „์œผ๋กœ ๋กค๋ฐฑ
alembic downgrade a1b2c3d4e5f6
 
# ๋ชจ๋“  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋กค๋ฐฑ (์ดˆ๊ธฐ ์ƒํƒœ๋กœ)
alembic downgrade base

8. ์ƒํƒœ ํ™•์ธ

# ํ˜„์žฌ ์ ์šฉ๋œ ๋ฆฌ๋น„์ „ ํ™•์ธ
alembic current
 
# ์ „์ฒด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํžˆ์Šคํ† ๋ฆฌ
alembic history
 
# ์ƒ์„ธ ํžˆ์Šคํ† ๋ฆฌ
alembic history --verbose

9. ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” op ํ•จ์ˆ˜

Alembic์˜ op ๋ชจ๋“ˆ์ด ์ œ๊ณตํ•˜๋Š” ์ฃผ์š” ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ ํ•จ์ˆ˜๋“ค์ด๋‹ค.

from alembic import op
import sqlalchemy as sa
 
# ํ…Œ์ด๋ธ” ์ƒ์„ฑ/์‚ญ์ œ
op.create_table('posts', sa.Column('id', sa.Integer, primary_key=True))
op.drop_table('posts')
 
# ์ปฌ๋Ÿผ ์ถ”๊ฐ€/์‚ญ์ œ/๋ณ€๊ฒฝ
op.add_column('users', sa.Column('age', sa.Integer))
op.drop_column('users', 'age')
op.alter_column('users', 'name', type_=sa.String(200))
 
# ์ธ๋ฑ์Šค ์ƒ์„ฑ/์‚ญ์ œ
op.create_index('ix_users_email', 'users', ['email'])
op.drop_index('ix_users_email')
 
# ์™ธ๋ž˜ํ‚ค ์ถ”๊ฐ€
op.create_foreign_key('fk_post_user', 'posts', 'users', ['user_id'], ['id'])
 
# ์ˆœ์ˆ˜ SQL ์‹คํ–‰
op.execute("UPDATE users SET active = true")

10. autogenerate๊ฐ€ ๊ฐ์ง€ํ•˜์ง€ ๋ชปํ•˜๋Š” ๊ฒƒ

์ฃผ์˜

autogenerate๋Š” ๋งŒ๋Šฅ์ด ์•„๋‹ˆ๋‹ค. ์•„๋ž˜ ํ•ญ๋ชฉ์€ ์ง์ ‘ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค.

  • ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ ์ด๋ฆ„ ๋ณ€๊ฒฝ (์‚ญ์ œ + ์ƒ์„ฑ์œผ๋กœ ์ธ์‹)
  • ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜)
  • DB ํŠนํ™” ๊ธฐ๋Šฅ (ํŒŒํ‹ฐ์…˜, ํŠธ๋ฆฌ๊ฑฐ, ์Šคํ† ์–ด๋“œ ํ”„๋กœ์‹œ์ € ๋“ฑ)
  • CHECK ์ œ์•ฝ ์กฐ๊ฑด ๋ณ€๊ฒฝ

11. ์‹ค๋ฌด ํŒ

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์€ ๋ฐ˜๋“œ์‹œ ์ปค๋ฐ‹ํ•œ๋‹ค

versions/ ๋””๋ ‰ํ† ๋ฆฌ์˜ ํŒŒ์ผ์€ git์— ํฌํ•จํ•ด์•ผ ํ•œ๋‹ค. ํŒ€์›๋“ค์ด ๊ฐ™์€ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ˆœ์„œ๋Œ€๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

ํ”„๋กœ๋•์…˜ ์ ์šฉ ์ „ ํ™•์ธ
# SQL๋งŒ ์ถœ๋ ฅํ•˜๊ณ  ์‹ค์ œ ์ ์šฉํ•˜์ง€ ์•Š์Œ
alembic upgrade head --sql

์ƒ์„ฑ๋œ SQL์„ ๋ฆฌ๋ทฐํ•œ ๋’ค ์ ์šฉํ•˜๋ฉด ์•ˆ์ „ํ•˜๋‹ค.

๋ธŒ๋žœ์น˜ ์ถฉ๋Œ ํ•ด๊ฒฐ

์—ฌ๋Ÿฌ ํŒ€์›์ด ๋™์‹œ์— ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ๋งŒ๋“ค๋ฉด down_revision์ด ์ถฉ๋Œํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด ๊ฒฝ์šฐ merge๋กœ ํ•ด๊ฒฐํ•œ๋‹ค.

alembic merge -m "merge migrations" revision1 revision2