들어가며
프로덕션 서버에 직접 접속해서 ALTER TABLE을 실행한 적이 있나요? "어제 스테이징에 반영한 스키마 변경이 프로덕션에는 안 되어 있어서 장애가 났습니다" 같은 보고를 받은 적이 있나요?
데이터베이스 스키마를 수동으로 관리하는 것은 애플리케이션 코드를 Git 없이 관리하는 것과 다를 바 없습니다. 스키마 변경 이력을 추적할 수 없고, 환경 간 불일치가 발생하며, 롤백도 불가능합니다.
DB 마이그레이션 도구는 이 문제를 해결합니다. 이 글에서는 가장 널리 사용되는 DB 마이그레이션 도구인 Flyway를 깊이 있게 다룹니다.
1. DB 마이그레이션이란
1.1 왜 필요한가
데이터베이스 마이그레이션이란 DB 스키마의 변경사항을 체계적으로 관리하고, 버전별로 순차 적용하는 프로세스입니다.
수동 관리의 문제점:
- 어떤 변경이 어느 환경에 적용되었는지 추적 불가
- 개발/스테이징/프로덕션 환경 간 스키마 불일치
- 여러 개발자가 동시에 스키마를 변경하면 충돌 발생
- 실수로 잘못된 DDL을 실행하면 롤백이 어려움
- 감사(Audit) 추적이 불가능
1.2 버전 관리의 중요성
애플리케이션 코드는 Git으로 버전 관리하면서 DB 스키마는 왜 관리하지 않을까요? DB 마이그레이션 도구는 스키마 변경을 코드로 관리하여, 누가 언제 어떤 변경을 했는지 추적할 수 있게 합니다.
app-code v1.0 → v1.1 → v1.2 → v2.0
↓ ↓ ↓ ↓
db-schema V1 → V2 → V3 → V4
이렇게 애플리케이션 버전과 DB 스키마 버전을 함께 관리하면, 어떤 시점의 코드가 어떤 스키마를 기대하는지 명확해집니다.
2. Flyway vs Liquibase vs Alembic
| 항목 | Flyway | Liquibase | Alembic |
|---|---|---|---|
| 언어 | Java | Java | Python |
| 스키마 정의 | SQL 파일 기반 | XML/YAML/JSON/SQL | Python 코드 |
| 학습 곡선 | 낮음 | 중간 | 중간 |
| DB 지원 | 20개 이상 | 50개 이상 | SQLAlchemy 지원 DB |
| 롤백 | 유료(Teams), 또는 수동 | 내장(auto-rollback) | 내장(downgrade) |
| Spring Boot 통합 | 네이티브 지원 | 네이티브 지원 | 해당 없음 |
| 가격 | Community 무료 | Community 무료 | 완전 무료 |
| 철학 | SQL 중심, 단순함 | 추상화 레이어 | ORM 연동 |
선택 기준:
- Flyway: SQL을 직접 작성하고 싶고, 단순한 것을 선호하는 팀. Java/Spring Boot 프로젝트에 최적
- Liquibase: DB 벤더 독립적인 스키마 관리가 필요한 팀. 자동 롤백이 중요한 경우
- Alembic: Python/SQLAlchemy 기반 프로젝트
3. Flyway 설치
3.1 CLI 설치
# macOS
brew install flyway
# Linux (tar.gz)
wget -qO- https://download.red-gate.com/maven/release/com/redgate/flyway/flyway-commandline/10.x/flyway-commandline-10.x-linux-x64.tar.gz | tar xz
sudo ln -s $(pwd)/flyway-10.x/flyway /usr/local/bin
# 버전 확인
flyway --version
3.2 Docker
docker run --rm flyway/flyway:10 -url=jdbc:postgresql://host:5432/mydb -user=admin -password=secret migrate
Docker Compose에서 사용하기:
services:
flyway:
image: flyway/flyway:10
command: migrate
volumes:
- ./sql:/flyway/sql
- ./conf:/flyway/conf
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_DB: mydb
POSTGRES_USER: admin
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d mydb"]
interval: 5s
timeout: 5s
retries: 5
3.3 Maven/Gradle
<!-- Maven pom.xml -->
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>10.15.0</version>
<configuration>
<url>jdbc:postgresql://localhost:5432/mydb</url>
<user>admin</user>
<password>secret</password>
</configuration>
</plugin>
// Gradle build.gradle
plugins {
id "org.flywaydb.flyway" version "10.15.0"
}
flyway {
url = 'jdbc:postgresql://localhost:5432/mydb'
user = 'admin'
password = 'secret'
}
3.4 Spring Boot 통합
Spring Boot에서는 의존성만 추가하면 자동으로 마이그레이션이 실행됩니다.
<!-- pom.xml -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
# application.yml
spring:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
baseline-version: '0'
4. 핵심 개념
4.1 마이그레이션 파일 네이밍
Flyway는 파일 이름 규칙으로 마이그레이션의 유형, 버전, 설명을 구분합니다.
V2__add_email_column.sql
│ │ └── 설명 (언더스코어 2개로 구분)
│ └── 버전 번호
└── 유형 (V=Versioned, U=Undo, R=Repeatable)
네이밍 규칙:
- 버전 구분자: 언더스코어 2개(
__) - 단어 구분: 언더스코어 1개(
_) - 버전 번호: 숫자, 점(
.), 언더스코어 사용 가능 - 예시:
V1__init.sql,V1.1__add_index.sql,V2_1__create_orders.sql
4.2 flyway_schema_history 테이블
Flyway는 어떤 마이그레이션이 적용되었는지 flyway_schema_history 테이블에 기록합니다.
SELECT installed_rank, version, description, checksum, installed_on, success
FROM flyway_schema_history
ORDER BY installed_rank;
installed_rank | version | description | checksum | installed_on | success
----------------+---------+-----------------------+-------------+----------------------+---------
1 | 1 | init | 1234567890 | 2026-04-01 10:00:00 | t
2 | 2 | add email column | -987654321 | 2026-04-05 14:30:00 | t
3 | 3 | create orders table | 1122334455 | 2026-04-10 09:15:00 | t
4.3 체크섬
Flyway는 각 마이그레이션 파일의 체크섬(CRC32)을 계산하여 기록합니다. 한번 적용된 마이그레이션 파일을 수정하면 체크섬이 변경되어 다음 실행 시 오류가 발생합니다.
이는 마이그레이션의 불변성을 보장합니다. 한번 적용된 마이그레이션은 절대 수정하지 말고, 새로운 마이그레이션을 추가해야 합니다.
5. 마이그레이션 유형
5.1 Versioned Migration (V)
가장 기본적인 마이그레이션으로, 버전 번호 순서대로 한 번만 실행됩니다.
-- V1__create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_users_username ON users(username);
5.2 Undo Migration (U) - Teams Edition
Versioned Migration의 역방향을 정의합니다. Flyway Teams(유료) 기능입니다.
-- U1__create_users_table.sql
DROP TABLE IF EXISTS users;
5.3 Repeatable Migration (R)
버전 번호가 없으며, 파일 내용이 변경될 때마다 재실행됩니다. 뷰, 함수, 프로시저 등의 관리에 적합합니다.
-- R__refresh_user_statistics_view.sql
CREATE OR REPLACE VIEW user_statistics AS
SELECT
u.id,
u.username,
COUNT(o.id) AS order_count,
COALESCE(SUM(o.total_amount), 0) AS total_spent,
MAX(o.created_at) AS last_order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.username;
5.4 Callback
마이그레이션 라이프사이클의 특정 시점에 실행되는 스크립트입니다.
sql/
beforeMigrate.sql -- 마이그레이션 시작 전
afterMigrate.sql -- 마이그레이션 완료 후
beforeEachMigrate.sql -- 각 마이그레이션 파일 실행 전
afterEachMigrate.sql -- 각 마이그레이션 파일 실행 후
beforeValidate.sql -- 유효성 검사 전
활용 예시 -- 마이그레이션 후 통계 갱신:
-- afterMigrate.sql
ANALYZE;
SELECT schemaname, tablename, last_analyze
FROM pg_stat_user_tables
WHERE schemaname = 'public';
6. Flyway 명령어
6.1 migrate
미적용 마이그레이션을 순서대로 실행합니다.
flyway migrate
6.2 info
현재 마이그레이션 상태를 보여줍니다.
flyway info
+-----------+---------+---------------------+----------+---------------------+----------+----------+
| Category | Version | Description | Type | Installed On | State | Undoable |
+-----------+---------+---------------------+----------+---------------------+----------+----------+
| Versioned | 1 | init | SQL | 2026-04-01 10:00:00 | Success | No |
| Versioned | 2 | add email column | SQL | 2026-04-05 14:30:00 | Success | No |
| Versioned | 3 | create orders table | SQL | | Pending | No |
+-----------+---------+---------------------+----------+---------------------+----------+----------+
6.3 validate
적용된 마이그레이션의 체크섬과 파일 시스템의 체크섬을 비교합니다.
flyway validate
CI/CD 파이프라인에서 마이그레이션을 실행하기 전에 validate를 먼저 실행하는 것이 좋습니다.
6.4 repair
실패한 마이그레이션 기록을 정리합니다.
flyway repair
주요 동작:
- 실패한 마이그레이션 기록을 flyway_schema_history에서 제거
- 적용된 마이그레이션의 체크섬을 현재 파일 기준으로 재계산
6.5 clean
주의: 모든 객체를 삭제합니다. 프로덕션에서 절대 사용 금지!
flyway clean
개발 환경에서 깨끗한 상태에서 다시 시작할 때 사용합니다.
Flyway 10부터는 flyway.cleanDisabled=false를 명시적으로 설정해야 합니다.
6.6 baseline
기존 DB에 Flyway를 도입할 때 사용합니다.
flyway baseline -baselineVersion=5 -baselineDescription="existing schema"
이미 V1~V5까지의 스키마가 적용된 DB에 Flyway를 도입하면, baseline을 V5로 설정하고 V6부터 새 마이그레이션을 추가합니다.
7. 설정 파일
7.1 flyway.conf
# flyway.conf
flyway.url=jdbc:postgresql://localhost:5432/mydb
flyway.user=admin
flyway.password=secret
flyway.schemas=public
flyway.locations=filesystem:sql
flyway.baselineOnMigrate=true
flyway.baselineVersion=0
flyway.connectRetries=3
flyway.cleanDisabled=true
flyway.outOfOrder=false
flyway.validateOnMigrate=true
7.2 Spring Boot (application.yml)
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: admin
password: secret
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
baseline-version: '0'
clean-disabled: true
validate-on-migrate: true
out-of-order: false
connect-retries: 3
table: flyway_schema_history
7.3 환경별 설정
Spring Boot에서는 프로파일별로 설정을 분리합니다.
# application-dev.yml
spring:
flyway:
clean-disabled: false
locations: classpath:db/migration,classpath:db/seed
# application-prod.yml
spring:
flyway:
clean-disabled: true
locations: classpath:db/migration
validate-on-migrate: true
8. 실전 예제
8.1 V1 -- 사용자 테이블 생성
-- V1__create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(255),
password_hash VARCHAR(255) NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT uq_users_username UNIQUE (username)
);
COMMENT ON TABLE users IS '사용자 정보';
COMMENT ON COLUMN users.username IS '로그인 아이디';
8.2 V2 -- 이메일 컬럼 제약조건 추가
-- V2__add_email_unique_constraint.sql
-- 기존 중복 데이터 정리
DELETE FROM users a
USING users b
WHERE a.id > b.id
AND a.email = b.email
AND a.email IS NOT NULL;
-- NOT NULL 제약조건 추가
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
-- UNIQUE 인덱스 추가
CREATE UNIQUE INDEX CONCURRENTLY idx_users_email ON users(email);
ALTER TABLE users ADD CONSTRAINT uq_users_email UNIQUE USING INDEX idx_users_email;
8.3 V3 -- 주문 테이블 생성
-- V3__create_orders_table.sql
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
order_number VARCHAR(20) NOT NULL UNIQUE,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
total_amount DECIMAL(12, 2) NOT NULL DEFAULT 0,
currency VARCHAR(3) NOT NULL DEFAULT 'KRW',
shipping_address TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT chk_orders_status CHECK (status IN ('PENDING', 'CONFIRMED', 'SHIPPED', 'DELIVERED', 'CANCELLED'))
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at DESC);
COMMENT ON TABLE orders IS '주문 정보';
8.4 R -- 뷰 갱신
-- R__refresh_views.sql
CREATE OR REPLACE VIEW v_user_order_summary AS
SELECT
u.id AS user_id,
u.username,
u.email,
COUNT(o.id) AS total_orders,
COALESCE(SUM(o.total_amount), 0) AS total_spent,
COALESCE(AVG(o.total_amount), 0) AS avg_order_amount,
MAX(o.created_at) AS last_order_at
FROM users u
LEFT JOIN orders o ON u.id = o.user_id AND o.status != 'CANCELLED'
GROUP BY u.id, u.username, u.email;
CREATE OR REPLACE VIEW v_daily_order_stats AS
SELECT
DATE(created_at) AS order_date,
COUNT(*) AS order_count,
SUM(total_amount) AS daily_revenue,
AVG(total_amount) AS avg_order_value,
COUNT(DISTINCT user_id) AS unique_customers
FROM orders
WHERE status NOT IN ('CANCELLED')
GROUP BY DATE(created_at);
8.5 프로젝트 디렉토리 구조
src/main/resources/
db/
migration/
V1__create_users_table.sql
V2__add_email_unique_constraint.sql
V3__create_orders_table.sql
V4__create_order_items_table.sql
V5__add_user_profile_fields.sql
R__refresh_views.sql
R__update_functions.sql
seed/
V100__insert_test_users.sql
V101__insert_test_orders.sql
9. 팀 협업 전략
9.1 브랜치별 마이그레이션 충돌
여러 개발자가 동시에 마이그레이션을 작성하면 버전 번호가 충돌할 수 있습니다.
타임스탬프 기반 버전 번호 사용:
V20260412_001__feature_a_create_table.sql (개발자 A)
V20260412_002__feature_b_add_column.sql (개발자 B)
또는 outOfOrder 허용:
spring:
flyway:
out-of-order: true
이 옵션을 켜면 V3 이후에 V2.5가 추가되어도 실행됩니다. 하지만 팀 내에서 명확한 규칙을 정하는 것이 우선입니다.
9.2 네이밍 컨벤션
팀에서 합의할 사항:
# 방법 1: 순차 번호
V1__create_users.sql
V2__add_index.sql
# 방법 2: 날짜 기반
V20260412.1__create_users.sql
V20260412.2__add_index.sql
# 방법 3: 티켓 번호 포함
V1__JIRA_123_create_users.sql
V2__JIRA_456_add_index.sql
9.3 코드 리뷰 체크리스트
마이그레이션 PR 리뷰 시 확인 사항:
- 파일 이름 규칙을 따르는가
- 기존 마이그레이션 파일을 수정하지 않았는가
- 대용량 테이블에 락을 유발하는 DDL은 없는가
- 롤백 스크립트가 준비되어 있는가
- 인덱스 생성 시 CONCURRENTLY 옵션을 사용했는가
10. CI/CD 연동
10.1 GitHub Actions
name: Database Migration
on:
push:
branches: [main]
paths:
- 'src/main/resources/db/migration/**'
jobs:
migrate:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Run Flyway Validate
run: |
docker run --rm --network host \
-v $(pwd)/src/main/resources/db/migration:/flyway/sql \
flyway/flyway:10 \
-url=jdbc:postgresql://localhost:5432/testdb \
-user=test \
-password=test \
validate
- name: Run Flyway Migrate
run: |
docker run --rm --network host \
-v $(pwd)/src/main/resources/db/migration:/flyway/sql \
flyway/flyway:10 \
-url=jdbc:postgresql://localhost:5432/testdb \
-user=test \
-password=test \
migrate
- name: Run Flyway Info
run: |
docker run --rm --network host \
-v $(pwd)/src/main/resources/db/migration:/flyway/sql \
flyway/flyway:10 \
-url=jdbc:postgresql://localhost:5432/testdb \
-user=test \
-password=test \
info
10.2 Kubernetes Init Container
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
spec:
initContainers:
- name: flyway-migrate
image: flyway/flyway:10
args: ["migrate"]
env:
- name: FLYWAY_URL
value: "jdbc:postgresql://postgres-service:5432/mydb"
- name: FLYWAY_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: FLYWAY_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
volumeMounts:
- name: migration-scripts
mountPath: /flyway/sql
containers:
- name: app
image: my-app:latest
volumes:
- name: migration-scripts
configMap:
name: flyway-migrations
11. 무중단 마이그레이션
11.1 위험한 DDL 작업
프로덕션 환경에서 주의해야 할 작업들:
| 작업 | 위험도 | 이유 |
|---|---|---|
| NOT NULL 추가 | 높음 | 전체 테이블 스캔 필요 |
| 컬럼 타입 변경 | 높음 | 전체 테이블 리라이트 |
| 인덱스 생성 | 중간 | 테이블 락 (CONCURRENTLY 미사용 시) |
| 컬럼 추가 (기본값 없음) | 낮음 | 메타데이터만 변경 |
| 컬럼 추가 (기본값 있음) | 중간 | PostgreSQL 11 이전에는 전체 리라이트 |
11.2 Expand-Contract 패턴
컬럼 이름을 변경하는 안전한 방법:
Phase 1 -- Expand (확장):
-- V10__expand_add_new_column.sql
ALTER TABLE users ADD COLUMN full_name VARCHAR(100);
-- 기존 데이터 복사
UPDATE users SET full_name = name WHERE full_name IS NULL;
-- 양쪽 컬럼에 쓰도록 트리거 설정
CREATE OR REPLACE FUNCTION sync_user_name()
RETURNS TRIGGER AS '
BEGIN
IF TG_OP = ''INSERT'' OR TG_OP = ''UPDATE'' THEN
IF NEW.name IS DISTINCT FROM OLD.name THEN
NEW.full_name := NEW.name;
END IF;
IF NEW.full_name IS DISTINCT FROM OLD.full_name THEN
NEW.name := NEW.full_name;
END IF;
END IF;
RETURN NEW;
END;
' LANGUAGE plpgsql;
CREATE TRIGGER trg_sync_user_name
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION sync_user_name();
Phase 2 -- 애플리케이션 배포:
새 코드는 full_name 컬럼을 사용하도록 변경합니다.
Phase 3 -- Contract (축소):
-- V11__contract_remove_old_column.sql
DROP TRIGGER IF EXISTS trg_sync_user_name ON users;
DROP FUNCTION IF EXISTS sync_user_name();
ALTER TABLE users DROP COLUMN name;
11.3 대용량 테이블 인덱스 추가
-- V12__add_index_concurrently.sql
-- Flyway에서 CONCURRENTLY를 사용하려면 트랜잭션을 비활성화해야 합니다
-- flyway.conf에서 설정: flyway.postgresql.transactional.lock=false
-- 또는 Spring Boot에서 별도의 non-transactional migration 사용
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_created_at_status
ON orders(created_at, status);
12. 트러블슈팅
12.1 체크섬 불일치
증상: Migration checksum mismatch 오류
원인: 이미 적용된 마이그레이션 파일이 수정됨
해결:
# 방법 1: repair로 체크섬 갱신 (파일 변경이 의도적인 경우)
flyway repair
# 방법 2: 원본 파일 복구 (실수로 변경한 경우)
git checkout -- src/main/resources/db/migration/V1__init.sql
12.2 실패한 마이그레이션 복구
증상: 마이그레이션이 중간에 실패하여 DB 상태가 불일치
# 1. 실패 상태 확인
flyway info
# 2. 실패한 마이그레이션 기록 제거
flyway repair
# 3. SQL 파일 수정 후 재실행
flyway migrate
PostgreSQL은 DDL이 트랜잭션을 지원하므로 실패 시 자동 롤백됩니다. 하지만 MySQL은 DDL이 암묵적으로 커밋되므로 수동 복구가 필요할 수 있습니다.
12.3 baseline 활용
기존 프로젝트에 Flyway를 도입하는 경우:
# 1. 현재 스키마를 V1으로 덤프
pg_dump --schema-only mydb > V1__baseline.sql
# 2. baseline 설정
flyway baseline -baselineVersion=1
# 3. 이후 마이그레이션은 V2부터 작성
12.4 흔한 실수들
실수 1: 적용된 마이그레이션 파일 수정
- 항상 새로운 마이그레이션을 추가하세요
실수 2: 버전 번호 건너뛰기 (V1, V3, V5)
- Flyway는 기본적으로 순차 번호를 기대합니다
실수 3: 개발 환경에서 clean 후 프로덕션 설정 복사
- 환경별로 설정 파일을 분리하세요
실수 4: 마이그레이션에 DML과 DDL 혼합
- 가능하면 분리하세요. DDL 실패 시 DML도 함께 롤백될 수 있습니다
13. 모범 사례
13.1 롤백 전략
Flyway Community에서는 자동 Undo가 없으므로, 수동 롤백 스크립트를 준비합니다.
sql/
migration/
V5__add_payment_table.sql
rollback/
V5__rollback_add_payment_table.sql
-- V5__rollback_add_payment_table.sql (수동 실행용)
DROP TABLE IF EXISTS payments;
DELETE FROM flyway_schema_history WHERE version = '5';
13.2 데이터 마이그레이션
스키마 변경과 데이터 변경은 별도의 마이그레이션으로 분리합니다.
-- V6__add_role_column.sql (DDL)
ALTER TABLE users ADD COLUMN role VARCHAR(20) DEFAULT 'USER';
-- V7__migrate_admin_roles.sql (DML)
UPDATE users SET role = 'ADMIN' WHERE username IN ('admin', 'superadmin');
UPDATE users SET role = 'MODERATOR' WHERE username IN ('mod1', 'mod2');
13.3 시드 데이터
개발/테스트 환경에서만 사용할 시드 데이터는 별도 디렉토리로 관리합니다.
# application-dev.yml
spring:
flyway:
locations:
- classpath:db/migration
- classpath:db/seed
-- db/seed/V1000__seed_test_users.sql
INSERT INTO users (username, email, password_hash, role) VALUES
('testuser1', 'test1@example.com', 'hashed_pw_1', 'USER'),
('testuser2', 'test2@example.com', 'hashed_pw_2', 'USER'),
('testadmin', 'admin@example.com', 'hashed_pw_3', 'ADMIN')
ON CONFLICT (username) DO NOTHING;
13.4 전체 체크리스트
마이그레이션 작성 시 확인할 사항:
- 이전에 적용된 마이그레이션을 수정하지 않았는가
- 버전 번호가 기존 마이그레이션과 충돌하지 않는가
- 프로덕션 대용량 테이블에 대한 ALTER TABLE에 락 영향을 검토했는가
- 인덱스는 CONCURRENTLY 옵션으로 생성하는가
- 롤백 스크립트가 준비되어 있는가
- 데이터 마이그레이션과 스키마 마이그레이션이 분리되어 있는가
- 환경별 설정이 올바른가
마무리
Flyway는 단순하면서도 강력한 DB 마이그레이션 도구입니다. SQL 파일 기반의 접근 방식은 DBA와 개발자 모두에게 친숙하며, Spring Boot와의 네이티브 통합은 Java 생태계에서 특히 빛을 발합니다.
핵심 원칙을 정리하면:
- 마이그레이션은 불변이다: 한번 적용된 파일은 절대 수정하지 않는다
- 항상 전진한다: 문제가 있으면 새 마이그레이션으로 수정한다
- 환경을 분리한다: dev, staging, prod 설정을 명확히 구분한다
- 팀 규칙을 정한다: 네이밍, 리뷰 프로세스, 롤백 전략을 합의한다
- CI/CD에 통합한다: 수동 실행은 실수의 시작이다
DB 스키마 관리도 결국 소프트웨어 엔지니어링의 일부입니다. 코드처럼 버전 관리하고, 테스트하고, 자동화하세요.
현재 단락 (1/519)
프로덕션 서버에 직접 접속해서 ALTER TABLE을 실행한 적이 있나요?