Flyway 마이그레이션이란?
Flyway는 데이터베이스 스키마 변경을 버전 관리하는 마이그레이션 도구다. Git이 소스 코드를 추적하듯, Flyway는 SQL 스크립트의 실행 이력을 flyway_schema_history 테이블에 기록하고, 순서대로 적용되지 않은 마이그레이션만 실행한다. Spring Boot는 Flyway를 자동 감지하여 애플리케이션 시작 시 마이그레이션을 수행한다.
기본 설정과 네이밍 컨벤션
# build.gradle.kts
dependencies {
implementation("org.flywaydb:flyway-core")
// PostgreSQL 사용 시
implementation("org.flywaydb:flyway-database-postgresql")
}
# application.yml
spring:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true # 기존 DB에 Flyway 도입 시
baseline-version: 0 # 베이스라인 버전
validate-on-migrate: true # 체크섬 검증
out-of-order: false # 순서 강제
clean-disabled: true # clean 명령 비활성화 (프로덕션 필수!)
datasource:
url: jdbc:postgresql://localhost:5432/myapp
username: app_user
password: ${DB_PASSWORD}
-- 파일 네이밍 규칙:
-- V{버전}__{설명}.sql → 버전 마이그레이션 (한 번만 실행)
-- R__{설명}.sql → 반복 마이그레이션 (변경될 때마다 실행)
-- U{버전}__{설명}.sql → 언두 마이그레이션 (Teams 에디션)
-- src/main/resources/db/migration/V1__create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
role VARCHAR(20) DEFAULT 'USER',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
-- V2__create_orders_table.sql
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
total_amount DECIMAL(12,2) NOT NULL,
status VARCHAR(20) DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
버전 번호는 V1, V2 같은 정수형 또는 V1.1, V2026.03.15.1 같은 날짜형을 사용할 수 있다. 팀에서 충돌을 방지하려면 타임스탬프 기반(V20260315190200)이 유리하다.
환경별 마이그레이션 분리
# application.yml — 환경별 location 설정
spring:
flyway:
locations:
- classpath:db/migration # 공통 스키마
- classpath:db/migration/{vendor} # DB 벤더별 (postgresql, mysql)
---
# application-dev.yml
spring:
flyway:
locations:
- classpath:db/migration
- classpath:db/seed # 개발용 시드 데이터
---
# application-test.yml
spring:
flyway:
locations:
- classpath:db/migration
- classpath:db/testdata # 테스트 데이터
-- src/main/resources/db/seed/R__seed_data.sql
-- R(Repeatable): 내용 변경 시 다시 실행됨
INSERT INTO users (email, name, role) VALUES
('admin@test.com', 'Admin', 'ADMIN'),
('user@test.com', 'User', 'USER')
ON CONFLICT (email) DO NOTHING;
Java 기반 마이그레이션
복잡한 데이터 변환이나 조건부 로직은 SQL만으로 어렵다. Flyway의 Java Migration으로 해결할 수 있다.
// V5__encrypt_user_emails.java
package db.migration;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import java.sql.*;
public class V5__encrypt_user_emails extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
try (Statement select = context.getConnection().createStatement();
ResultSet rows = select.executeQuery(
"SELECT id, email FROM users WHERE encrypted_email IS NULL")) {
try (PreparedStatement update = context.getConnection().prepareStatement(
"UPDATE users SET encrypted_email = ? WHERE id = ?")) {
while (rows.next()) {
long id = rows.getLong("id");
String email = rows.getString("email");
String encrypted = encrypt(email); // 커스텀 암호화
update.setString(1, encrypted);
update.setLong(2, id);
update.addBatch();
}
update.executeBatch();
}
}
}
private String encrypt(String value) {
// 암호화 로직
return "enc:" + value;
}
}
콜백: 마이그레이션 라이프사이클 훅
@Component
@Slf4j
public class FlywayCallbackConfig implements Callback {
@Override
public boolean supports(Event event, Context context) {
return event == Event.AFTER_MIGRATE
|| event == Event.AFTER_MIGRATE_ERROR
|| event == Event.BEFORE_VALIDATE;
}
@Override
public boolean canHandleInTransaction(Event event, Context context) {
return true;
}
@Override
public void handle(Event event, Context context) {
switch (event) {
case AFTER_MIGRATE ->
log.info("✅ 마이그레이션 완료. 적용 버전: {}",
context.getMigrationInfo() != null
? context.getMigrationInfo().getVersion()
: "none");
case AFTER_MIGRATE_ERROR ->
log.error("❌ 마이그레이션 실패! 수동 확인 필요");
case BEFORE_VALIDATE ->
log.info("🔍 마이그레이션 체크섬 검증 시작");
}
}
}
// Bean 등록
@Configuration
public class FlywayConfig {
@Bean
public FlywayMigrationStrategy flywayMigrationStrategy(
FlywayCallbackConfig callback) {
return flyway -> Flyway.configure()
.configuration(flyway.getConfiguration())
.callbacks(callback)
.load()
.migrate();
}
}
무중단 마이그레이션 전략
프로덕션에서 테이블 구조를 변경할 때 다운타임 없이 마이그레이션하는 패턴이다.
-- 예: users.name → users.first_name + users.last_name 분리
-- Step 1 (V10): 새 컬럼 추가 (기존 코드 영향 없음)
ALTER TABLE users ADD COLUMN first_name VARCHAR(50);
ALTER TABLE users ADD COLUMN last_name VARCHAR(50);
-- Step 2 (V11): 데이터 마이그레이션 (배치)
UPDATE users
SET first_name = split_part(name, ' ', 1),
last_name = COALESCE(split_part(name, ' ', 2), '')
WHERE first_name IS NULL;
-- Step 3: 애플리케이션 배포 — 새 컬럼 읽기/쓰기 + 기존 컬럼도 쓰기
-- (코드에서 both columns write 구현)
-- Step 4 (V12): 기존 컬럼 제거 (모든 인스턴스가 새 코드일 때)
ALTER TABLE users DROP COLUMN name;
| 위험한 작업 | 안전한 대안 |
|---|---|
ALTER TABLE RENAME COLUMN |
새 컬럼 추가 → 데이터 복사 → 이전 컬럼 제거 (3단계) |
ALTER TABLE ALTER COLUMN TYPE |
새 컬럼 추가 → 트리거로 동기화 → 전환 |
DROP NOT NULL 역방향 |
기본값 설정 후 NOT NULL 추가 |
| 대용량 INDEX 생성 | CREATE INDEX CONCURRENTLY (PostgreSQL) |
멀티 모듈 프로젝트 설정
// 모듈별 독립 마이그레이션 (멀티 데이터소스)
@Configuration
public class MultiDataSourceFlywayConfig {
@Bean
@Primary
public Flyway primaryFlyway(@Qualifier("primaryDataSource") DataSource ds) {
return Flyway.configure()
.dataSource(ds)
.locations("classpath:db/migration/primary")
.schemas("public")
.table("flyway_primary_history")
.load();
}
@Bean
public Flyway analyticsFlyway(@Qualifier("analyticsDataSource") DataSource ds) {
return Flyway.configure()
.dataSource(ds)
.locations("classpath:db/migration/analytics")
.schemas("analytics")
.table("flyway_analytics_history")
.load();
}
// 애플리케이션 시작 시 모든 Flyway 실행
@Bean
public FlywayMigrationInitializer primaryInit(
@Qualifier("primaryFlyway") Flyway flyway) {
return new FlywayMigrationInitializer(flyway);
}
@Bean
public FlywayMigrationInitializer analyticsInit(
@Qualifier("analyticsFlyway") Flyway flyway) {
return new FlywayMigrationInitializer(flyway);
}
}
CI/CD 파이프라인 통합
# GitHub Actions — PR 시 마이그레이션 검증
name: Migration Check
on: pull_request
jobs:
validate:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: test
ports: ["5432:5432"]
steps:
- uses: actions/checkout@v4
- name: Run Flyway Validate
run: |
./gradlew flywayValidate
-Dflyway.url=jdbc:postgresql://localhost:5432/testdb
-Dflyway.user=postgres
-Dflyway.password=test
- name: Run Flyway Migrate
run: |
./gradlew flywayMigrate
-Dflyway.url=jdbc:postgresql://localhost:5432/testdb
-Dflyway.user=postgres
-Dflyway.password=test
Flyway는 JPA 엔티티와 DB 스키마 간의 진실의 원천(Source of Truth)을 SQL 파일에 둔다. ddl-auto=validate와 함께 사용하면 엔티티와 실제 스키마 불일치를 시작 시점에 감지할 수 있어 프로덕션 안정성이 크게 향상된다.