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: |
- 그 외 설정들은 공식 DOC 참고 https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
첫 실습때는 “비어있는 새로 만든 데이터베이스” 를 사용하길 권장한다.
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