Skip to content

필사 모드: 스키마 마이그레이션 도구 비교 — Flyway, Liquibase, golang-migrate, Alembic, Atlas

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

애플리케이션 코드는 Git으로 버전 관리하면서, 정작 데이터베이스 스키마는 누군가의 손으로 직접 ALTER TABLE을 실행하던 시절이 있었습니다. 그 결과 운영 DB와 스테이징 DB의 스키마가 미묘하게 달라지고, 새로 합류한 팀원의 로컬 환경은 또 다른 상태가 되었습니다. 스키마 마이그레이션 도구는 바로 이 문제를 해결하기 위해 등장했습니다.

스키마 마이그레이션 도구의 핵심 약속은 단순합니다. "스키마 변경을 코드처럼 버전 관리하고, 어떤 환경에서든 동일한 순서로 재현 가능하게 적용한다"는 것입니다. 이 글에서는 가장 널리 쓰이는 다섯 가지 도구인 Flyway, Liquibase, golang-migrate, Alembic, Atlas를 살펴봅니다. 각 도구가 어떤 모델을 채택했는지, 버전 관리와 체크섬은 어떻게 다루는지, 롤백과 CI 통합은 어디까지 지원하는지를 비교하겠습니다.

도구를 선택하기 전에 알아두면 좋은 점이 있습니다. 마이그레이션 도구에는 크게 두 가지 철학이 있습니다. 하나는 "변경 단위"를 차곡차곡 쌓아가는 **버전 기반(versioned)** 방식이고, 다른 하나는 "최종 상태"를 선언하면 도구가 변경 경로를 계산하는 **선언적(declarative)** 방식입니다. 이 차이를 이해하는 것이 도구 선택의 출발점입니다.

핵심 개념

버전 기반 마이그레이션

버전 기반 마이그레이션은 가장 직관적인 모델입니다. 스키마를 바꾸고 싶을 때마다 새로운 마이그레이션 파일을 작성하고, 도구는 이 파일들을 순서대로 한 번씩만 적용합니다. 적용 여부는 별도의 추적 테이블에 기록됩니다.

예를 들어 Flyway는 schema_history 테이블을, golang-migrate는 schema_migrations 테이블을 만들어 어느 버전까지 적용되었는지 기록합니다. 다음에 도구를 실행하면 아직 적용되지 않은 마이그레이션만 골라서 실행합니다.

[V1] 테이블 생성 -> 적용 완료 (history에 기록)

[V2] 컬럼 추가 -> 적용 완료 (history에 기록)

[V3] 인덱스 추가 -> 아직 미적용 <- 다음 실행 때 이것만 적용

이 모델의 장점은 변경 이력이 그대로 남는다는 것입니다. 누가 언제 어떤 변경을 했는지 마이그레이션 파일 자체가 증거가 됩니다. 단점은 파일이 계속 누적되고, 두 개발자가 동시에 같은 버전 번호로 마이그레이션을 만들면 충돌이 발생한다는 점입니다.

선언적 마이그레이션

선언적 마이그레이션은 접근이 다릅니다. 개발자는 "스키마가 최종적으로 이렇게 생겼으면 좋겠다"는 목표 상태만 선언합니다. 도구가 현재 DB 상태와 목표 상태를 비교(diff)해서 그 사이를 메우는 마이그레이션을 자동으로 생성합니다. Atlas와 Prisma의 일부 워크플로가 이 방식을 채택합니다.

현재 스키마(DB) + 목표 스키마(선언 파일)

| |

+-------- diff -----+

|

자동 생성된 변경 계획

선언적 방식의 매력은 "지금 무엇이 달라져야 하는지"를 사람이 직접 계산하지 않아도 된다는 것입니다. 다만 자동 생성된 변경이 항상 안전한 것은 아니므로, 생성된 마이그레이션을 사람이 검토하는 단계가 반드시 필요합니다.

체크섬과 무결성

대부분의 도구는 이미 적용된 마이그레이션 파일의 내용을 **체크섬**(보통 CRC32 또는 해시)으로 기록합니다. 다음에 실행할 때 파일 내용이 바뀌었으면 체크섬이 달라지므로 오류를 냅니다. 이는 "이미 적용된 마이그레이션을 몰래 수정하면 안 된다"는 불변 규칙을 강제하는 장치입니다. 적용된 마이그레이션은 수정하지 말고, 새 마이그레이션을 추가하는 것이 원칙입니다.

Flyway — 단순한 SQL 우선 도구

Flyway는 "SQL이 곧 진실"이라는 철학을 가진 도구입니다. 대부분의 마이그레이션을 평범한 SQL 파일로 작성하며, 파일 이름 규칙이 곧 버전이 됩니다.

파일 이름 규칙

Flyway의 versioned 마이그레이션은 V로 시작하는 엄격한 이름 규칙을 따릅니다.

V1__create_users_table.sql

V2__add_email_to_users.sql

V2.1__add_index_on_email.sql

R__refresh_reporting_view.sql

V는 versioned, R은 repeatable(반복 가능, 매번 체크섬이 바뀌면 재실행) 마이그레이션을 뜻합니다. 버전 번호 다음에는 밑줄 두 개, 그 뒤에 설명이 옵니다.

실제 예시

-- V1__create_users_table.sql

CREATE TABLE users (

id BIGSERIAL PRIMARY KEY,

email VARCHAR(255) NOT NULL UNIQUE,

created_at TIMESTAMPTZ NOT NULL DEFAULT now()

);

-- V2__add_status_to_users.sql

ALTER TABLE users

ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'active';

적용은 명령 한 줄이면 됩니다.

flyway -url=jdbc:postgresql://localhost:5432/app \

-user=app -password=secret migrate

Flyway는 flyway_schema_history 테이블에 각 버전의 체크섬, 적용 시각, 실행 시간을 기록합니다. Community 에디션은 무료지만 자동 롤백(undo) 기능은 Teams/Enterprise 유료 에디션에서만 제공됩니다. 무료 버전에서는 롤백용 마이그레이션을 직접 작성해 앞으로 적용하는 방식으로 대응합니다.

베이스라인

이미 데이터가 들어있는 기존 DB에 Flyway를 도입할 때는 baseline 명령으로 시작점을 정합니다. 이렇게 하면 그 이전의 스키마는 이미 존재한다고 간주하고, 이후 마이그레이션만 적용합니다.

flyway -baselineVersion=1 baseline

Liquibase — changelog 중심의 멀티 포맷 도구

Liquibase는 SQL을 직접 쓰는 대신 **changelog**라는 추상 변경 단위로 스키마를 기술합니다. changelog는 XML, YAML, JSON, 또는 SQL 형식으로 작성할 수 있습니다. 각 변경 단위는 changeSet으로 불리며, 고유한 id와 author를 가집니다.

XML changelog 예시

xmlns="http://www.liquibase.org/xml/ns/dbchangelog"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

YAML changelog 예시

같은 변경을 YAML로 쓰면 다음과 같습니다.

databaseChangeLog:

- changeSet:

id: 1

author: youngju

changes:

- createTable:

tableName: users

columns:

- column:

name: id

type: BIGINT

autoIncrement: true

constraints:

primaryKey: true

nullable: false

추상 changelog의 가장 큰 장점은 **데이터베이스 독립성**입니다. createTable 한 줄이 PostgreSQL에서는 BIGSERIAL로, MySQL에서는 AUTO_INCREMENT로 적절히 변환됩니다. 여러 종류의 DB를 동시에 지원해야 하는 제품이라면 이 점이 큰 매력입니다.

롤백

Liquibase는 롤백을 일급 시민으로 다룹니다. createTable 같은 변경은 역연산을 자동으로 추론하며, 복잡한 변경은 rollback 블록을 직접 명시할 수 있습니다.

liquibase --changeLogFile=changelog.xml rollbackCount 1

Liquibase는 DATABASECHANGELOG 테이블에 적용 이력을, DATABASECHANGELOGLOCK 테이블로 동시 실행을 방지하는 락을 관리합니다.

golang-migrate — 가볍고 명시적인 up/down

golang-migrate는 Go 생태계에서 널리 쓰이는 경량 도구입니다. 철학은 명료합니다. 모든 마이그레이션은 up과 down 두 개의 SQL 파일로 이루어지며, 도구는 그 SQL을 그대로 실행할 뿐입니다.

파일 구조

migrations/

000001_create_users.up.sql

000001_create_users.down.sql

000002_add_status.up.sql

000002_add_status.down.sql

-- 000001_create_users.up.sql

CREATE TABLE users (

id BIGSERIAL PRIMARY KEY,

email VARCHAR(255) NOT NULL UNIQUE

);

-- 000001_create_users.down.sql

DROP TABLE users;

실행

migrate -database "postgres://app:secret@localhost:5432/app?sslmode=disable" \

-path ./migrations up

migrate -database "postgres://app:secret@localhost:5432/app?sslmode=disable" \

-path ./migrations down 1

golang-migrate는 schema_migrations 테이블에 현재 버전과 dirty 플래그를 기록합니다. 마이그레이션 도중 실패하면 dirty 상태로 남고, 사용자가 force 명령으로 상태를 정리한 뒤 다시 시도해야 합니다. 롤백은 down 파일을 직접 작성한 만큼만 가능하므로, down SQL을 성실하게 작성하는 습관이 중요합니다. 자동 체크섬 검증은 기본 제공되지 않으므로, 적용된 마이그레이션을 수정하지 않는 규율은 팀이 스스로 지켜야 합니다.

Alembic — SQLAlchemy를 위한 ORM 마이그레이션

Alembic은 Python의 SQLAlchemy ORM과 짝을 이루는 마이그레이션 도구입니다. 마이그레이션을 Python 코드로 작성하며, 가장 강력한 기능은 ORM 모델과 실제 DB를 비교해 마이그레이션을 **자동 생성**하는 autogenerate입니다.

revision 파일

각 마이그레이션은 revision 식별자와 down_revision으로 연결된 체인을 이룹니다. 이 체인이 적용 순서를 결정합니다.

"""add status to users

Revision ID: a1b2c3d4

Revises: 9f8e7d6c

"""

from alembic import op

revision = "a1b2c3d4"

down_revision = "9f8e7d6c"

def upgrade():

op.add_column(

"users",

sa.Column("status", sa.String(20), nullable=False, server_default="active"),

)

def downgrade():

op.drop_column("users", "status")

autogenerate

모델을 수정한 뒤 다음 명령을 실행하면 Alembic이 차이를 감지해 마이그레이션 초안을 만들어 줍니다.

alembic revision --autogenerate -m "add status to users"

alembic upgrade head

alembic downgrade -1

autogenerate는 편리하지만 완벽하지 않습니다. 컬럼 타입 변경이나 제약 조건 같은 일부 변경은 감지하지 못할 수 있으므로, 생성된 마이그레이션은 반드시 사람이 검토해야 합니다. Alembic은 alembic_version 테이블에 현재 revision을 기록합니다.

Prisma Migrate와의 비교

Node 생태계의 Prisma Migrate도 ORM 기반 마이그레이션의 좋은 예입니다. schema.prisma 파일이 모델의 단일 진실 공급원이 되고, prisma migrate dev 명령이 SQL 마이그레이션을 생성합니다. Alembic이 Python 코드로 변경을 표현하는 반면, Prisma는 선언적 스키마 파일과 생성된 SQL을 함께 관리한다는 차이가 있습니다.

Atlas — 선언적 스키마와 자동 diff

Atlas는 비교적 새로운 도구로, 선언적 마이그레이션을 전면에 내세웁니다. 개발자는 HCL(또는 SQL, ORM 정의)로 원하는 최종 스키마를 선언하고, Atlas가 현재 상태와 비교해 마이그레이션을 생성합니다.

HCL 스키마 선언

schema "public" {}

table "users" {

schema = schema.public

column "id" {

type = bigint

null = false

}

column "email" {

type = varchar(255)

null = false

}

primary_key {

columns = [column.id]

}

index "idx_users_email" {

unique = true

columns = [column.email]

}

}

diff로 마이그레이션 생성

스키마 파일을 수정한 뒤 migrate diff를 실행하면, Atlas가 기존 마이그레이션 디렉터리와 목표 스키마의 차이를 계산해 새 마이그레이션 파일을 만들어 줍니다.

atlas migrate diff add_status \

--dir "file://migrations" \

--to "file://schema.hcl" \

--dev-url "docker://postgres/16/dev"

atlas migrate apply \

--dir "file://migrations" \

--url "postgres://app:secret@localhost:5432/app?sslmode=disable"

Atlas는 dev-url로 지정한 임시 DB에 스키마를 실제로 적용해 보면서 정확한 diff를 계산합니다. 또한 versioned 워크플로와 선언적 워크플로를 모두 지원하므로, 팀의 성숙도에 맞춰 선택할 수 있습니다. atlas migrate lint 같은 정적 분석으로 위험한 변경(예: 컬럼 삭제, 비가역적 변경)을 사전에 잡아내는 기능도 강점입니다.

도구 비교 표

다섯 도구의 특성을 한눈에 정리하면 다음과 같습니다. 셀은 단순 텍스트로만 표기합니다.

| 항목 | Flyway | Liquibase | golang-migrate | Alembic | Atlas |

| --- | --- | --- | --- | --- | --- |

| 마이그레이션 모델 | SQL files | XML/YAML changelog | up/down SQL | Python code | declarative HCL |

| 주 생태계 | JVM | JVM | Go | Python | Go, multi |

| 자동 diff 생성 | No | Partial | No | Yes (autogenerate) | Yes |

| 롤백 지원 | Paid only | Yes | Manual down | Yes | Yes |

| 체크섬 검증 | Yes | Yes | No | No | Yes |

| 멀티 DB 추상화 | Partial | Yes | Partial | Yes (via ORM) | Yes |

| 베이스라인 | Yes | Yes | Partial | Partial | Yes |

| CI 친화도 | High | High | High | High | High |

| 선언적 모드 | No | No | No | No | Yes |

위 표에서 "Partial"은 부분 지원 또는 우회 방법이 필요함을 뜻합니다.

CI 통합

어떤 도구를 쓰든, 마이그레이션은 CI 파이프라인에서 자동으로 적용하거나 최소한 검증해야 합니다. 일반적인 패턴은 다음과 같습니다.

[코드 푸시] -> [CI 빌드] -> [임시 DB 기동]

-> [마이그레이션 적용] -> [테스트 실행]

-> [통과 시 배포] -> [운영 DB에 마이그레이션 적용]

운영 환경에서는 애플리케이션 배포와 마이그레이션 적용의 순서를 신중히 정해야 합니다. 컬럼 추가처럼 하위 호환되는 변경은 먼저 적용해도 안전하지만, 컬럼 삭제처럼 호환을 깨는 변경은 신구 버전이 공존하는 무중단 배포에서 단계적으로 진행해야 합니다.

무중단 배포를 위한 확장-수축 패턴

확장-수축(expand-contract) 패턴은 호환성을 깨지 않고 스키마를 바꾸는 표준 기법입니다.

1단계(확장): 새 컬럼/테이블을 추가한다 (구버전과 신버전 모두 동작)

2단계(이중 쓰기): 신버전이 새 구조에도 데이터를 쓴다

3단계(백필): 과거 데이터를 새 구조로 채운다

4단계(전환): 모든 읽기를 새 구조로 옮긴다

5단계(수축): 더 이상 쓰이지 않는 옛 컬럼을 제거한다

충돌 해결

버전 기반 도구에서 가장 흔한 운영 사고는 **버전 충돌**입니다. 두 개발자가 각자 브랜치에서 동일한 다음 버전 번호로 마이그레이션을 만들면, 머지할 때 같은 버전이 두 개가 됩니다.

해결책은 도구마다 조금씩 다릅니다. Flyway는 같은 버전 번호를 허용하지 않으므로, 머지 시점에 한쪽의 번호를 올려야 합니다. Alembic은 선형 버전 대신 revision 그래프를 쓰므로 분기가 생길 수 있고, 이때 alembic merge 명령으로 두 분기를 하나로 합치는 머지 revision을 만듭니다. Atlas는 선언적 스키마가 단일 진실 공급원이므로, 충돌이 SQL 마이그레이션이 아니라 스키마 파일 수준에서 일어나 Git 머지로 해결하기가 비교적 수월합니다.

가장 실용적인 예방책은 타임스탬프 기반 파일 이름을 쓰는 것입니다. 버전 번호 대신 생성 시각(예: 20260616T101500)을 쓰면 두 개발자가 동시에 만들어도 번호가 겹치지 않습니다. golang-migrate와 Alembic은 이 방식을 기본 또는 옵션으로 지원합니다.

선택 가이드

상황별로 다음과 같이 정리할 수 있습니다.

JVM 기반 프로젝트이고 SQL을 직접 통제하고 싶다면 **Flyway**가 가장 단순한 선택입니다. 파일 이름 규칙만 익히면 바로 쓸 수 있고, 진입 장벽이 낮습니다.

여러 종류의 DB를 동시에 지원해야 하거나, 롤백을 표준 워크플로로 삼고 싶다면 **Liquibase**가 강합니다. changelog 추상화가 DB 차이를 흡수해 줍니다.

Go 서비스이고 의존성을 최소화하면서 SQL을 완전히 통제하고 싶다면 **golang-migrate**가 잘 맞습니다. 단, down 파일과 무결성 규율은 팀이 직접 챙겨야 합니다.

Python과 SQLAlchemy를 쓴다면 사실상 **Alembic**이 표준입니다. autogenerate로 생산성이 높지만, 생성 결과 검토는 필수입니다. Node와 Prisma를 쓴다면 Prisma Migrate가 같은 자리를 차지합니다.

선언적 워크플로를 시도하거나, 마이그레이션 정적 분석으로 위험한 변경을 사전에 걸러내고 싶다면 **Atlas**가 가장 현대적인 선택입니다. 기존 versioned 도구와 함께 도입하기도 좋습니다.

흔한 함정

마지막으로 어떤 도구를 쓰든 공통으로 마주치는 함정들을 정리합니다.

첫째, **이미 적용된 마이그레이션을 수정**하는 것입니다. 체크섬 검증이 있는 도구라면 오류로 막아주지만, 없는 도구에서는 환경마다 스키마가 어긋나는 조용한 재앙이 됩니다. 적용된 것은 절대 고치지 말고 새 마이그레이션을 추가하세요.

둘째, **롤백을 과신**하는 것입니다. DROP COLUMN을 되돌리는 down 마이그레이션이 컬럼을 다시 만들 수는 있어도, 사라진 데이터까지 되살리지는 못합니다. 비가역적 변경은 롤백이 아니라 사전 백업과 단계적 배포로 대비해야 합니다.

셋째, **거대한 테이블에 잠금이 걸리는 변경**입니다. 일부 ALTER TABLE은 테이블 전체를 잠가 서비스 장애를 일으킵니다. 큰 테이블에는 온라인 DDL이나 단계적 마이그레이션 전략을 적용해야 합니다.

넷째, **마이그레이션을 테스트하지 않는 것**입니다. 마이그레이션도 코드입니다. CI에서 임시 DB에 적용해 보고, 가능하면 운영과 유사한 데이터로 리허설하는 것이 안전합니다.

마치며

스키마 마이그레이션 도구는 결국 같은 목표를 향합니다. 스키마 변경을 코드처럼 다루어, 어떤 환경에서든 안전하고 재현 가능하게 적용하는 것입니다. Flyway의 단순함, Liquibase의 멀티 DB 추상화, golang-migrate의 명시성, Alembic의 ORM 통합, Atlas의 선언적 모델은 각각 다른 강점을 가집니다.

정답은 팀의 언어 생태계, DB 다양성, 그리고 선언적 워크플로에 대한 선호에 따라 달라집니다. 중요한 것은 도구 선택 자체보다, 마이그레이션을 버전 관리하고 CI로 검증하며 적용된 변경을 불변으로 다루는 규율입니다. 그 규율이 잡혀 있다면 어떤 도구를 골라도 충분히 안전하게 운영할 수 있습니다.

참고 자료

- Flyway 공식 문서: https://flywaydb.org/documentation/

- Liquibase 공식 문서: https://docs.liquibase.com/

- golang-migrate 저장소: https://github.com/golang-migrate/migrate

- Alembic 공식 문서: https://alembic.sqlalchemy.org/

- Atlas 공식 문서: https://atlasgo.io/

- Prisma Migrate 문서: https://www.prisma.io/docs/orm/prisma-migrate

- PostgreSQL ALTER TABLE 문서: https://www.postgresql.org/docs/current/sql-altertable.html

- MySQL 공식 문서: https://dev.mysql.com/doc/

현재 단락 (1/179)

애플리케이션 코드는 Git으로 버전 관리하면서, 정작 데이터베이스 스키마는 누군가의 손으로 직접 ALTER TABLE을 실행하던 시절이 있었습니다. 그 결과 운영 DB와 스테이징 ...

작성 글자: 0원문 글자: 8,906작성 단락: 0/179