
이 글에서는 Alembic 마이그레이션이 프로덕션에서 실패했을 때 무엇부터 확인하고 어떤 순서로 되돌려야 하는지를 정리합니다. 결론부터 말씀드리면, 스키마를 먼저 되돌리기 전에 애플리케이션 코드 롤백 여부와 alembic_version 테이블의 실제 상태부터 확인하는 게 순서입니다. 순서를 반대로 하면 스키마는 되돌렸는데 코드는 새 스키마를 기대하는 상태가 되어 장애가 더 커지는 경우가 많습니다.
이 내용은 Alembic 1.13 계열, SQLAlchemy 2.x, PostgreSQL 14 이상 환경을 기준으로 설명합니다. MySQL/MariaDB는 DDL 문의 트랜잭션 처리 방식이 달라 일부 대응이 다르게 적용됩니다.
Alembic 마이그레이션이 프로덕션에서 실패하는 흔한 원인
프로덕션에서만 실패하고 스테이징에서는 멀쩡했던 마이그레이션은 대부분 데이터 양과 락(lock) 문제에서 비롯됩니다. 스테이징 테이블은 수천 건이지만 프로덕션 테이블은 수백만 건이라, NOT NULL 컬럼을 추가하면서 백필(backfill) 쿼리를 함께 실행하면 락 대기 시간 초과로 트랜잭션이 강제 종료되는 일이 흔합니다.
또 다른 원인은 배포 순서입니다. 롤링 배포 중에는 구버전 코드와 신버전 코드가 동시에 떠 있는 구간이 생기는데, 이 사이에 마이그레이션이 컬럼을 삭제하거나 이름을 바꾸면 아직 구버전 코드를 쓰는 인스턴스가 존재하지 않는 컬럼을 참조하다 에러를 냅니다. 스키마 자체는 정상적으로 적용됐는데도 애플리케이션 레벨에서 실패가 터지는 패턴입니다.
되돌리기 전에 실제 상태부터 확인해야 합니다
에러 로그를 보자마자 alembic downgrade -1부터 치는 건 위험합니다. 먼저 alembic current와 alembic history --verbose로 지금 DB가 어느 리비전에 멈춰 있는지, 그리고 실행하려는 다운그레이드가 어떤 리비전으로 되돌아가는지부터 확인해야 합니다.
$ alembic current
Current revision(s) for postgresql://prod-db/app:
abc123f4e5d6 (head)
$ alembic history --verbose
Rev: abc123f4e5d6 (head)
Parent: def456a1b2c3
Path: versions/20240912_1230_add_status_column.py
add status column to orders

PostgreSQL은 대부분의 DDL 문도 트랜잭션 안에서 처리되기 때문에, 마이그레이션이 중간에 예외로 죽으면 자동으로 롤백되어 스키마가 이전 상태로 남아 있는 경우가 많습니다. 이때는 alembic current가 이미 이전 리비전을 가리키고 있으니 downgrade 명령 자체가 필요 없을 수 있습니다. 반면 MySQL은 컬럼 추가·삭제 같은 DDL이 실행되는 순간 암묵적 커밋이 발생해서, 여러 단계 중 일부만 반영된 채로 멈추는 경우가 생깁니다.
안전하게 되돌리는 순서
상태를 확인했으면 아래 순서로 진행하는 편이 안전합니다.
- 트래픽을 새 코드로 더 이상 늘리지 않도록 배포 파이프라인을 멈춥니다. 이미 배포된 신버전 인스턴스가 있다면 먼저 구버전으로 롤백할지, 스키마를 유지한 채 앞으로 진행할지 판단합니다.
- 문제가 된 리비전 파일을 열어
downgrade()함수가 실제로 구현돼 있는지 확인합니다. 자동 생성된 마이그레이션 중에는downgrade()가pass로 비어 있거나 원본 컬럼을 복원만 하고 데이터는 되살리지 못하는 경우가 있습니다. downgrade()가 정상적으로 구현돼 있으면 아래처럼 한 단계씩 되돌립니다.
$ alembic downgrade -1
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.runtime.migration] Running downgrade abc123f4e5d6 -> def456a1b2c3, add status column to orders
$ alembic current
def456a1b2c3 (head)
Alembic은 리비전 파일마다 upgrade()와 downgrade()를 쌍으로 정의해 두면 명령 한 줄로 다운그레이드를 실행할 수 있게 설계돼 있습니다. 다만 한 단계(-1)씩 내려가면서 매번 alembic current로 검증하는 편이, 여러 단계를 한 번에 내리다가 어디서 실패했는지 못 찾는 상황보다 훨씬 다루기 쉽습니다.
downgrade만으로 안 되는 경우, 트레이드오프를 알아야 합니다
컬럼을 삭제하는 마이그레이션은 downgrade()에서 같은 이름과 타입으로 컬럼을 다시 만들 수는 있어도, 삭제되기 전에 들어 있던 데이터까지 복원해주지는 않습니다. op.drop_column은 그 순간 데이터를 지워버리기 때문에, downgrade가 “성공”해도 빈 컬럼만 돌아오는 셈입니다. 이런 케이스는 마이그레이션 실행 전 스냅샷이나 백업이 없으면 데이터를 되살릴 방법이 없습니다.
아래 표는 상황별로 어떤 조치가 현실적인지 정리한 것입니다.

| 상황 | 실제로 벌어지는 일 | 권장 조치 |
|---|---|---|
| PostgreSQL, 트랜잭션 도중 실패 | 자동 롤백되어 스키마 변경 없음 | 코드 수정 후 재실행하면 됨 |
| MySQL, DDL 도중 일부만 반영 | alembic_version과 실제 스키마 불일치 |
스키마를 직접 조회해 수동으로 정합 |
downgrade()가 온전히 구현됨 |
alembic downgrade -1 정상 동작 |
한 단계씩 내리며 검증 |
| 컬럼·테이블 삭제형 마이그레이션 | 구조는 복원돼도 데이터는 유실 | 사전 백업(pg_dump 등)에서 복원 필요 |
또 하나 짚어야 할 트레이드오프는 이미 여러 애플리케이션 인스턴스가 새 스키마를 전제로 코드를 돌리고 있는 상황입니다. 이때 스키마만 downgrade해버리면, 아직 내려가지 않은 신버전 인스턴스가 없는 컬럼을 참조하며 새로운 에러를 만들어냅니다. 이 경우는 스키마 롤백보다 애플리케이션 롤백을 먼저 끝내는 게 안전합니다.
alembic_version 테이블이 꼬였을 때는 어떻게 하나요?
MySQL 환경에서 DDL 일부만 반영된 채 멈추면, alembic_version 테이블에 기록된 리비전과 실제 테이블 구조가 서로 다른 이야기를 하는 상태가 됩니다. 이럴 때 alembic stamp <revision> 명령으로 버전 테이블 값을 강제로 원하는 리비전으로 맞출 수 있습니다.
$ alembic stamp def456a1b2c3
다만 이 명령은 실제 마이그레이션 코드를 전혀 실행하지 않고 버전 기록만 덮어씁니다. 실제 테이블 구조를 \d orders 같은 명령으로 눈으로 확인해서, stamp로 지정하려는 리비전과 정말 일치하는지 검증한 다음에 실행해야 합니다. 확인 없이 stamp만 찍으면 다음 마이그레이션이 이미 존재하는 컬럼을 다시 만들려다 또 실패하는 악순환이 생깁니다.
다음 배포 전 롤백 계획을 문서에 남겨두세요
정리하면, Alembic 마이그레이션이 프로덕션에서 실패했을 때는 스키마부터 되돌리지 말고 alembic current와 alembic history로 실제 상태를 확인하는 게 먼저입니다. downgrade()가 데이터까지 복원해주는지 리비전 파일을 직접 읽어 판단하고, 안 된다면 백업 복원을 병행해야 합니다. 롤링 배포 중이라면 스키마 롤백보다 애플리케이션 롤백 순서를 먼저 정리하는 편이 2차 장애를 막습니다.
바로 실행할 수 있는 다음 단계로는, 이번에 문제가 된 리비전 파일의 downgrade()를 스테이징에서 직접 실행해 보고 데이터 유실 여부를 기록해 두는 것을 추천합니다. 그리고 앞으로 새 마이그레이션을 머지하기 전에 CI에서 alembic downgrade -1까지 자동으로 검증하는 단계를 추가해 두면, 다음 실패는 지금보다 훨씬 빨리 원인을 좁힐 수 있습니다.
