공부하고 기록하는, 경제학과 출신 개발자의 노트

프로그래밍/이것저것_개발일지

FastAPI, SQLAlchemy 프로덕트에서 alembic을 쓰지 않은 이유

inspirit941 2024. 1. 29. 11:42
반응형

작은 사이드 프로젝트 중간에 투입돼서 코드 개선을 하게 되었는데,

FastAPI + SQLAlchemy (postgres) + Alembic으로 구성된 기존 방식에서 Alembic을 atlas로 변경했다.

 

 

Python으로 구성하는 API Server + ORM + DB migration tool 조합의 정석으로 많이 쓰이지만,

글 작성 기준 alembic 최신버전인 v1.13.1 프로덕트를 써보니 장점보다는 단점이 더 크게 느껴졌다.

 

 

당연하게도, 내가 느낀 단점이나 불편함이 alembic 프로덕트 자체의 한계는 아닐 수도 있다. 문서에서 내가 놓친 부분이 있을 수도 있다.

이런 고민을 하게 된 상황은 아래와 같은 조건에서였다.

  • Python으로 production level의 웹 서비스를 목표로 상정하고 개발해본 경험은 없는 팀 구성원들과 협업해야 함.
  • alembic의 모든 기능을 샅샅이 써보고, 일일이 비교한 뒤에 결정한 것이 아님. 그럴 시간이 없었다.
  • 규모가 작은 사이드 프로젝트 팀 특성상, 회사의 DBA 같은 DB management 전문 인력이 없다

 


Alembic이란?

https://github.com/sqlalchemy/alembic

 

GitHub - sqlalchemy/alembic: A database migrations tool for SQLAlchemy.

A database migrations tool for SQLAlchemy. Contribute to sqlalchemy/alembic development by creating an account on GitHub.

github.com

 

 

 

https://alembic.sqlalchemy.org/en/latest/

 

Welcome to Alembic’s documentation! — Alembic 1.13.1 documentation

 

alembic.sqlalchemy.org

 

sqlalchemy 개발한 쪽에서 만드는 database migration tool.

SQLAlchemy 진영에서 만들었기 때문에 Python ORM으로 SQLAlchmey를 쓰면 자연스럽게 접하게 된다.

 

yourproject/
    alembic/
        env.py
        README
        script.py.mako
        versions/
            3512b954651e_add_account.py
            2b1ae634e5cd_add_order_id.py
            3adcc9a56557_rename_username_field.py
  • Python ORM으로 정의한 Object를 감지해서, DB schema로 변경해주는 python script를 생성해준다.
    offline mode를 사용하면 python script 말고 sql를 만들어준다.
  • revision 단위로 DB 변경사항의 버전관리를 할 수 있다.

하지만, 소규모 팀에서 DBA 없이 DB management 형상관리를 수행하기에는
alembic이 지원하지 않는 기능의 단점이 너무 크게 다가왔다.

 

 

단점 1. Lint 기능 없음

alembic을 걷어내야겠다는 생각을 하게 된 근본적인 이유.

 

제대로 된 DBA 없이 API 서버의 DB management를 감당해야 하는 우리 팀 특성상

  • 개발자가 Pull Request 했을 때,
    프로젝트에 정의된 ORM과 DB schema 형상이 일치하는지를
    CI / CD 프로세스에서 자동으로 확인해야 한다.

  • 변경이력에 문제가 없다면 Pass, conflict가 발생할 경우 Fail을 CI / CD 프로세스에서 리턴할 수 있어야 한다.

스크린샷 2024-02-04 오후 12 31 48스크린샷 2024-02-04 오후 12 32 07

 

 

그런데, alembic 명령어에는 lint 비슷한 기능을 수행할 방법이 없다. dry run 옵션조차 없다.

  • upgrade 명령어에 --sql 옵션을 쓰면 sql 파일을 만들어주는데,
    이 sql 파일을 DB에 직접 dry run 요청하는 스크립트를 만드는 식으로 구현해야 한다.

  • 개발자가 DB에 직접 sql문으로 요청을 보내는 작업을 하지 않으려고 ORM을 쓰는 건데
    lint 체크하려고 dry run을 DB 서버에 직접 요청해야만 하는 구조는 불합리하게 느껴졌다.

문서: https://alembic.sqlalchemy.org/en/latest/api/commands.html

 

Commands — Alembic 1.13.1 documentation

Commands Note this section discusses the internal API of Alembic as regards its command invocation system. This section is only useful for developers who wish to extend the capabilities of Alembic. For documentation on using Alembic commands, please see Tu

alembic.sqlalchemy.org



단점 2. ORM 변경사항이 없으면 빈 python script를 생성함

lint가 안 되면, alembic revision --autogenerate 명령어를 실행했을 때

  • ORM과 DB의 형상이 일치했을 때엔 No-Op으로 동작해 준다면
  • migration 파일 디렉토리에 아무런 변경사항이 없는지 체크하는 식으로 우회할 수도 있을 것이다.

그런데 alembic은 revision autogenerate 명령어를 실행하면, 아무런 동작도 하지 않는 python script를 생성한다.

예시: https://alembic.sqlalchemy.org/en/latest/tutorial.html#create-a-migration-script

"""create account table

Revision ID: 1975ea83b712
Revises:
Create Date: 2011-11-08 11:40:27.089406

"""

# revision identifiers, used by Alembic.
revision = '1975ea83b712'
down_revision = None
branch_labels = None

from alembic import op
import sqlalchemy as sa

def upgrade():
    pass

def downgrade():
    pass

 

 

매번 alembic 명령어를 실행할 때마다 아무 의미없는 python script가 만들어지면

  • CI / CD로 실행한 결과가 누적되었을 때
  • 어떤 Python script가 유의미한 ORM 변경사항이 포함된 코드인지 어떻게 알 수 있나?

 

alembic은 merge라는 옵션을 써서 revision 이력을 하나로 합쳐주는 명령어가 있다.

  • 하지만, 무의미한 python script를 생성하는 것 자체가 version management 관점에서 불필요한 기능이다.
  • '이게 불편하면 너가 불필요한 py 파일을 합쳐서 써라' 라는 식의 접근법처럼 느껴지기도 했다.

 

단점 3. 본인들이 문서에 적어놓은 alembic autogenerate 한계점

https://alembic.sqlalchemy.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect

 

Auto Generating Migrations — Alembic 1.13.1 documentation

Alembic can view the status of the database and compare against the table metadata in the application, generating the “obvious” migrations based on a comparison. This is achieved using the --autogenerate option to the alembic revision command, which pl

alembic.sqlalchemy.org

 

autogenerate 옵션에서

  • alembic이 변경사항을 감지할 수 있는 것
  • Optionally Detect할 수 있는 것
  • Cannot Detect하는 것

무슨 조건들이 많이 걸려 있었다.

 

DB 전담으로 관리해줄 팀원이 없어서 migration tool에 전적으로 의존해야 하는 팀 사정상,

"되는 게 있고, 안되는 게 있고, 특정 상황에서만 되는 게 있는" tool을 믿고 써야 할 이유가 없다고 느꼈다.

 

 

대안으로 선택한 tool : atlas

https://github.com/ariga/atlas

 

GitHub - ariga/atlas: A modern tool for managing database schemas

A modern tool for managing database schemas. Contribute to ariga/atlas development by creating an account on GitHub.

github.com

atlas: go로 작성된 Declarative, Language-agnostic DB migration tool

 

회사에서 GORM의 migration 기능을 대체하기 위한 프로덕트를 리서치할 때 찾아뒀던 건데, sqlAlchemy가 최근에 지원되기 시작했다.

 

https://atlasgo.io/guides/orms/sqlalchemy

 

Automatic migration planning for SQLAlchemy | Atlas | Open-source database schema management tool

TL;DR

atlasgo.io

https://github.com/ariga/atlas/releases/tag/v0.18.0

(2024. 01.12일 v0.18 버전에서부터 공식 지원)

lint 기능 지원

개발자가 ORM을 수정한 뒤, migration을 위한 sql 파일을 생성하지 않고 pull request를 올렸을 때,

CI 에서 lint 명령어로 Pass / Fail 여부를 확인할 수 있다.

 

스크린샷 2024-01-30 오전 10 29 07

성공 예시

 

 

 

스크린샷 2024-01-30 오전 9 34 08

 

 

실패 예시

 

 

CI / CD 프로세스에 DB migration 로직 추가해서 자동화 가능

스크린샷 2024-02-04 오후 1 07 56

 

스크린샷 2024-02-04 오후 1 07 49

코드리뷰를 마치고 dev 브랜치에 머지되면, CD 과정에서 자동으로 Dev DB에 migration을 수행하도록 Pipeline을 구축할 수 있다.

 

 

 

그럼에도 약간 아쉬운 점: major 버전 release가 없다는 불안감, pip 패키지의 기능 부족, cli 의존성이 높음.

atlas 자체는 go로 빌드된 cli tool이고, cli 사용을 상정한 방식으로 구현되어 있다.

 

  • sqlalchemy를 지원하기 위한 pip 패키지를 설치해도, (v0.18 버전 기준) python 코드 내에서 migration을 실행하는 방법이 없다.
  • 공식문서를 봐도, atlas pip 패키지는 python ORM을 import하기 위한 최소한의 인터페이스만 제공한다.
    atlas 자체의 기능은 결국 cli binary를 설치해서 사용해야 한다.

공식문서의 사용법 (https://atlasgo.io/guides/orms/sqlalchemy) 을 보면 단적으로 알 수 있는데

# filename: load_models.py

# import one of the models
from models import User
from atlas_provider_sqlalchemy.ddl import print_ddl

print_ddl("mysql", [User])

load_models.py 라는 파일을 프로젝트 내에 만들어두고

 

data "external_schema" "sqlalchemy" {
    program = [
        "python3",
        "load_models.py"
    ]
}

env "sqlalchemy" {
  src = data.external_schema.sqlalchemy.url
  dev = "docker://mysql/8/dev"
  migration {
    dir = "file://migrations"
  }
  format {
    migrate {
      diff = "{{ sql . \"  \" }}"
    }
  }
}

 

cli에서 사용하는 atlas.hcl 파일을 프로젝트 디렉토리에 생성해줘야 한다.

atlas migrate diff --env sqlalchemy 를 터미널에서 실행하면

 

 

 

$ tree
migrations
|-- 20230918143104.sql
`-- atlas.sum

0 directories, 2 files

 

 

위와 같이 migrations 디렉토리 하위에 atlas가 관리하는 sql 파일 / 무결성 검사를 위한 sum 파일이 생성된다.

따라서 이 기능을 CI / CD에 적용하려면, CI / CD가 돌아가는 서버 (github action 같은) 에 atlas binary를 반드시 설치해줘야 한다.

 

 

 

결론

데이터베이스를 붙여야 하는 Python 웹 서비스를 검색하면,
보통은 기술 스택으로 FastAPI + SQLAlchemy + Alembic을 많이 소개한다.

 

 

하지만 내가 속한 사이드 프로젝트 팀에 필요한 것들이 무엇인지 파악한 뒤

  • 우리에게 필요한 기능을 제공하지 않는 Alembic 대신
  • 우리 상황에 필요한 기능을 제공하는 atlas로 대체한다.

라는 방향으로 프로젝트 개선을 제안했고,

DB management 관련 기능을 CI / CD 프로세스에 붙여서 데이터베이스 관리 부담을 줄였다.

반응형