Alembic

1. 소개

  • Python에서 사용할 수 있는 데이터베이스 마이그레이션 도구 (SQLAlchemy 생태계)
  • 데이터베이스의 스키마 변경 이력을 관리한다.
  • SQLAlchemy로 정의된 스키마를 실제 DB에 반영하거나, 이전 버전으로 되돌리는 기능도 포함된다.
  • 쉽게 말해, DB 테이블 스키마 변경을 Git 커밋처럼 버전 관리하는 도구라고 할 수 있다.

DB 마이그레이션 : 한 상태에서 다른 상태로 데이터베이스를 관리하고 이동시키는 일련의 과정.
여기에는 단순히 데이터를 옮기는 것 뿐 아니라, 데이터의 구조(스키마)를 변경하거나 적용하는 것도 포함된다.
Alembic은 데이터 자체를 옮기는 게 아닌, 데이터의 구조(스키마)를 다루는 툴이다.

2. 핵심 개념

항목 설명
마이그레이션
Migration
• DB 스키마를 한 상태에서 다른 상태로 변경하는 행위 또는 절차
마이그레이션 스크립트
Migration Script
• DB 스키마의 변경 사항이 담긴 개별 스크립트 파일
리비전
Revision
• 마이그레이션 스크립트가 생성될 때 부여되는 고유 버전 ID
• Git의 커밋과 유사한 역할
헤드
Head
• 가장 최신의 리비전(Revision)
업그레이드
Upgrade
• 마이그레이션에 정의된 스키마 변경사항대로 변경을 수행하는 것
다운그레이드
Downgrade
• 이전 스키마로 되돌리는 것
버전 테이블
Version Table
• DB 내에 현재 어떤 리비전까지 적용되었는지 기록하는 테이블
• 보통 alembic_version 이라는 이름의 테이블로 생성된다.

사용법

1. 설치

1
2
3
4
5
# pip
pip install alembic

# uv
uv add alembic

2. 초기화

  • 프로젝트에 마이그레이션 환경을 설정하고, 마이그레이션을 위한 파일과 디렉터리 구조를 생성한다.
1
2
3
4
5
# pip 설치시
alembic init alembic

# uv 설치시 (이후는 pip 기준으로 기재함)
uv run alembic init alembic


  • 위 명령어를 실행했을 때 생성되는 디렉터리와 파일 구조는 다음과 같다.
1
2
3
4
5
6
7
8
├── root
├── alembic
│    ├── versions
│    ├── env.py
│    ├── README
│    └── script.py.mako
├── alembic.ini
...
  • alembic : Alembic 마이그레이션 환경의 홈 디렉터리. 초기화시에 이름을 지정할 수 있다.
  • version : 마이그레이션 스크립트가 저장되는 디렉터리
  • env.py : Alembic이 실행될 때마다 호출하는 파이썬 스크립트로, SQLAlchemy에 대한 엔진 설정 정보 등이 포함된다.
  • script.py.mako : 새로운 마이그레이션 스크립트를 생성하는 데 새용되는 Mako 템플릿 파일
  • alembic.ini : Alembic의 메인 설정 파일


3. 설정 파일 (alembic.ini)

  • 초기화 단계에서 생성된 alembic.ini 파일에서 설정을 수행한다.
  • 이 파일은 Alembic이 실행될 떄, 파이썬의 configparser 패키지를 통해 읽힌다.
  • 많은 설정 항목들이 있지만, 주요하게 봐야 하는 건 DB URL 항목이다.
  • sqlalchemy.url 항목에 연결하고자 하는 DB에 대한 URL을 넣어주면 된다.
    1
    2
    3
    4
    
    # database URL.  This is consumed by the user-maintained env.py script only.
    # other means of configuring database URLs may be customized within the env.py
    # file.
    sqlalchemy.url = driver://user:pass@localhost/dbname
    


  • 주요 DB 에 대한 URL 형식은 다음과 같다.
DB 종류 URL 형식
PostgreSQL • postgresql://user:password@host:port/dbname
• postgresql+psycopg2://user:password@host:port/dbname
MySQL • mysql://user:password@host:port/dbname
• mysql+pymysql://user:password@host:port/dbname
MariaDB • mariadb://user:password@host:port/dbname
• mariadb+pymysql://user:password@host:port/dbname
Oracle • oracle+cx_oracle://user:password@host:port/?service_name=hr
MSSQL • mssql+pyodbc://user:password@host:port/dbname?driver=ODBC+Driver+17+for+SQL+Server
SQLite sqlite:///파일위치 (e.g. sqlite:///./app.db)
SQLite 메모리 sqlite:///:memory:


첫 실습때는 “비어있는 새로 만든 데이터베이스” 를 사용하길 권장한다.


4. 설정 파일 (env.py)

  • alembic/env.py 파일에 관리할 데이터 모델을 지정해준다.
1
2
3
4
5
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None    # <--- 여기


  • 예를 들어, 아래와 같은 데이터 모델을 정의했다고 해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# models/__init__.py

from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import Integer, String, ForeignKey

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"
    id    : Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="사용자 고유 번호")
    name  : Mapped[str] = mapped_column(String(50), index=True, comment="사용자 이름")
    email : Mapped[str] = mapped_column(String(100), comment="사용자 이메일")
    age   : Mapped[int] = mapped_column(Integer, comment="사용자 나이", nullable=True)

class Plant(Base):
    __tablename__ = "plants"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="식물의 고유 번호")
    name: Mapped[str] = mapped_column(String(50), comment="식물의 이름")
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), comment="소유 사용자 고유 번호")

    
from models.activity import Activity # -> 외부 파일에 선언한 데이터 모델이 있는 경우, 꼭 대표 파일에 import 할것을 권장(추후 복잡도 감소 목적)


  • 이 경우, alembic/env.py 파일에 아래와 같이 작성해준다.
1
2
3
4
5
6
7
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from models import *                           # 전체 데이터모델 import. 단, 꼭 Base가 포함되도록
from models import Base, User, Plant, Activity # 또는 직접 지정도 가능
target_metadata = Base.metadata


5. 첫 번째 마이그레이션 생성

  • 이제 위에서 정의한 데이터모델을 적용하는 마이그레이션을 생성해보자.
1
alembic revision --autogenerate -m "유저, 식물, 관리활동 테이블 생성"
  • alembic revision --autogenerate -m "메시지" 명령어로 새로운 마이그레이션을 생성할 수 있다.
  • --autogenerate 는 실제 DB와 코드상 데이터모델의 차이를 비교해 마이그레이션 파일을 자동으로 작성해주는 옵션이다.
  • --autogenerate 옵션을 사용하지 않으면, 마이그레이션 스크립트 템플리 파일만 생성되며, 업그레이드와 다운그레이드 사항을 수동으로 입력해줘야 한다.


  • 위 명령어를 실행하면 alembic/versions 디렉터리에 새로운 마이그레이션 스크립트 파일이 생성된 것을 볼 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# alembic/versions/1234a1234b12_유저_식물_관리활동_테이블_생성.py

"""유저, 식물, 관리활동 테이블 생성

Revision ID: 1234a1234b12
Revises: 
Create Date: 2026-05-03 00:33:13.376139

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision: str = '1234a1234b12'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

def upgrade() -> None:
    """Upgrade schema."""
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('users',
    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='사용자 고유 번호'),
    sa.Column('name', sa.String(length=50), nullable=False, comment='사용자 이름'),
    sa.Column('email', sa.String(length=100), nullable=False, comment='사용자 이메일'),
    sa.Column('age', sa.Integer(), nullable=True, comment='사용자 나이'),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_index(op.f('ix_users_name'), 'users', ['name'], unique=False)
    op.create_table('plants',
    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='식물의 고유 번호'),
    sa.Column('name', sa.String(length=50), nullable=False, comment='식물의 이름'),
    sa.Column('user_id', sa.Integer(), nullable=False, comment='소유 사용자 고유 번호'),
    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_table('관리활동',
    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='관리활동 ID'),
    sa.Column('user_id', sa.Integer(), nullable=False, comment='관리활동 사용자 ID'),
    sa.Column('plant_id', sa.Integer(), nullable=False, comment='관리활동 식물 ID'),
    sa.Column('activity_type', sa.String(length=10), nullable=False, comment='관리활동 유형'),
    sa.Column('activity_content', sa.String(length=2000), nullable=False, comment='관리활동 내용'),
    sa.ForeignKeyConstraint(['plant_id'], ['plants.id'], ),
    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
    sa.PrimaryKeyConstraint('id')
    )
    # ### end Alembic commands ###

def downgrade() -> None:
    """Downgrade schema."""
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('관리활동')
    op.drop_table('plants')
    op.drop_index(op.f('ix_users_name'), table_name='users')
    op.drop_table('users')
    # ### end Alembic commands ###


6. upgrade - 마이그레이션을 DB에 적용

  • 이제 마이그레이션 스크립트 내용을 실제 DB에 적용해볼 차례이다.
1
alembic upgrade head
  • alembic upgrade head 명령어를 사용하면 가장 최신의 마이그레이션을 DB에 적용하며
  • alembic upgrade <revision> 과 같이 특정 마이그레이션의 리비전을 지정해 적용할 수도 있다.


  • 명령어를 실행했을 때, 아래와 같은 출력이 나오면 된다.
1
2
3
INFO  [alembic.runtime.migration] Context impl MariaDBImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 1234a1234b12, 유저, 식물, 관리활동 테이블 생성


  • 실제 DB를 확인해보면, 새로운 테이블이 생성된 것을 볼 수 있다.


7. DB 스키마 수정을 위한 마이그레이션 생성

  • 그런데 테이블명이 이상하다. “관리활동” 이라는 한글로 된 테이블명이 보인다.
  • 아뿔싸 코드에 작성한 데이터모델에 실수로 테이블이름을 한글로 넣어버린 것이다.
  • 이를 수정하기 위해 아래와 같이 코드를 수정했다.
1
2
3
4
5
6
7
8
# 수정 전
class Activity(Base):
    __tablename__ = "관리활동"
    ...

# 수정 후
class Activity(Base):
    __tablename__ = "activities" # <--- 수정


  • 수정사항을 적용하기 위해 마이그레이션을 생성하고, DB에 적용한다.
1
2
3
4
5
# 마이그레이션 생성
alembic revision --autogenerate -m "관리활동 테이블 테이블명 수정(관리활동 -> activities)"

# DB에 적용
alembic upgrade head


  • 수정 완료


주의 : 테이블명을 변경하는 경우 데이터 소실

  • 이렇게 테이블 이름이 변경된 경우, 데이터가 사라질 수 있다! 주의!
  • 테이블 이름이 바뀌는 게 아니라, 새로운 테이블을 만들고 예전 이름 테이블을 DROP 하는 방식으로 작동되기 때문
  • 따라서 테이블명 변경 마이그레이션은 반드시 주의해서 적용해야 한다.


8. downgrade - 이전 마이그레이션으로 되돌리기

  • alembic downgrade 명령어로 이전 마이그레이션으로 되돌릴 수 있으며, 세 가지 사용법이 있다.
1
2
3
4
5
6
7
8
# 상대적인 단계로 되돌리기 (예시 : 한 단계 전)
alembic downgrade -1

# 특정 리비전으로 되돌리기
alembic downgrade <revision_id>

# 모든 마이그레이션 취소하기(최초 상태로 초기화)
alembic downgrade base


  • 다운그레이드를 수행하면, 아래와 같은 출력을 볼 수 있다.
1
2
3
INFO  [alembic.runtime.migration] Context impl MariaDBImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running downgrade dcb617629723 -> 1234a1234b12, 관리활동 테이블 테이블명 수정(관리활동 -> activities)


  • DB를 보면 다시 테이블 이름이 되돌려져있다. (실제로는 테이블 자체를 생성 및 삭제한 것)


9. current - 현재 리비전 확인

  • 현재 리비전 확인을 위해서는 alembic current 명령어를 이용할 수 있다.
  • 특히 다운그레이드 이후에는 리비전을 확인하는 단계를 두는 게 안전하다.
1
alembic current
1
2
3
INFO  [alembic.runtime.migration] Context impl MariaDBImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
1234a1234b12


Reference

Tutorial — Alembic 1.18.4 documentation
Tutorial — Alembic 1.18.4 documentation
Tutorial — Alembic 1.18.4 documentation

Comments