Spring Flyway 마이그레이션 심화

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와 함께 사용하면 엔티티와 실제 스키마 불일치를 시작 시점에 감지할 수 있어 프로덕션 안정성이 크게 향상된다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux