Skip to content
Published on

Spring Boot Flyway 완전 가이드: DB 마이그레이션 전략과 실전 패턴

Authors

1. Flyway란?

Flyway는 데이터베이스 스키마의 버전 관리를 자동화하는 오픈소스 마이그레이션 도구입니다. 애플리케이션 코드와 DB 스키마 변경을 함께 추적하고, 팀원 모두가 동일한 DB 상태를 유지할 수 있도록 돕습니다.

Flyway vs Liquibase 비교

항목FlywayLiquibase
마이그레이션 형식SQL 또는 JavaXML, YAML, JSON, SQL
학습 곡선낮음보통
롤백 지원유료(Teams)무료(내장)
Spring Boot 통합자동 설정자동 설정
커뮤니티매우 활발활발

Flyway는 SQL 중심의 단순한 접근 방식과 낮은 학습 곡선으로 많은 Spring Boot 프로젝트에서 선호됩니다.

flyway_schema_history 테이블

Flyway는 flyway_schema_history 테이블을 통해 마이그레이션 실행 이력을 관리합니다. 각 마이그레이션의 버전, 체크섬, 실행 시각, 성공 여부를 기록합니다.

-- flyway_schema_history 테이블 구조 예시
SELECT installed_rank, version, description, type, script, checksum, installed_on, success
FROM flyway_schema_history
ORDER BY installed_rank;

2. 의존성 및 기본 설정

Maven 의존성

<!-- Maven pom.xml -->
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>

<!-- MySQL 사용 시 추가 -->
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-mysql</artifactId>
</dependency>

<!-- PostgreSQL은 flyway-core만으로 충분 -->

Gradle 의존성

// build.gradle
dependencies {
    implementation 'org.flywaydb:flyway-core'
    // MySQL 사용 시
    implementation 'org.flywaydb:flyway-mysql'
}

application.yml 기본 설정

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: myuser
    password: mypassword
    driver-class-name: org.postgresql.Driver
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true
    validate-on-migrate: true
    out-of-order: false
    table: flyway_schema_history
    encoding: UTF-8
    connect-retries: 3

주요 설정 항목 설명:

  • enabled: Flyway 자동 실행 활성화 여부
  • locations: 마이그레이션 스크립트 위치 (복수 지정 가능)
  • baseline-on-migrate: 기존 DB에 Flyway를 처음 적용할 때 현재 상태를 베이스라인으로 설정
  • validate-on-migrate: 마이그레이션 전 체크섬 검증
  • out-of-order: 버전 순서 무시 허용 여부 (기본값 false)
  • table: 히스토리 테이블 이름 커스터마이즈

3. 마이그레이션 파일 네이밍 규칙

마이그레이션 타입별 규칙

Versioned Migration (버전 마이그레이션):

V{version}__{description}.sql

예시:

  • V1__create_users_table.sql
  • V2__add_email_index.sql
  • V1.2.3__fix_user_status_column.sql

Undo Migration (실행 취소, Flyway Teams 전용):

U{version}__{description}.sql

예시:

  • U2__add_email_index.sql

Repeatable Migration (반복 실행 가능):

R__{description}.sql

예시:

  • R__create_views.sql
  • R__update_stored_procedures.sql

네이밍 규칙 상세

  • 버전 구분자는 숫자와 점/언더스코어 허용: V1, V1.1, V1_1 모두 유효
  • 설명 구분자는 언더스코어 2개 (__)
  • 설명 내 언더스코어(_)는 공백으로 표시됨
  • 대소문자 구분: V는 대문자 필수
src/main/resources/
└── db/
    └── migration/
        ├── V1__init_schema.sql
        ├── V2__add_user_status.sql
        ├── V3__insert_seed_data.sql
        ├── V4__add_indexes.sql
        └── R__create_reporting_views.sql

4. 실전 마이그레이션 예제

V1__init_schema.sql - 초기 스키마 생성

-- V1__init_schema.sql
CREATE TABLE users (
    id          BIGSERIAL PRIMARY KEY,
    username    VARCHAR(50)  NOT NULL UNIQUE,
    email       VARCHAR(255) NOT NULL UNIQUE,
    password    VARCHAR(255) NOT NULL,
    created_at  TIMESTAMP    NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMP    NOT NULL DEFAULT NOW()
);

CREATE TABLE products (
    id          BIGSERIAL PRIMARY KEY,
    name        VARCHAR(200) NOT NULL,
    description TEXT,
    price       DECIMAL(10, 2) NOT NULL CHECK (price >= 0),
    stock       INTEGER        NOT NULL DEFAULT 0,
    created_at  TIMESTAMP      NOT NULL DEFAULT NOW()
);

CREATE TABLE orders (
    id          BIGSERIAL PRIMARY KEY,
    user_id     BIGINT       NOT NULL REFERENCES users(id),
    total       DECIMAL(10, 2) NOT NULL,
    status      VARCHAR(20)  NOT NULL DEFAULT 'PENDING',
    created_at  TIMESTAMP    NOT NULL DEFAULT NOW()
);

V2__add_user_status.sql - 컬럼 추가

-- V2__add_user_status.sql
ALTER TABLE users
    ADD COLUMN status       VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
    ADD COLUMN last_login   TIMESTAMP,
    ADD COLUMN phone_number VARCHAR(20);

-- 기존 데이터에 기본값 적용 (이미 DEFAULT로 설정되어 있지만 명시적으로)
UPDATE users SET status = 'ACTIVE' WHERE status IS NULL;

V3__insert_seed_data.sql - 초기 데이터 삽입

-- V3__insert_seed_data.sql
INSERT INTO products (name, description, price, stock) VALUES
    ('노트북', '고성능 개발자용 노트북', 1500000, 50),
    ('마우스', '무선 에르고노믹 마우스', 80000, 200),
    ('키보드', '기계식 RGB 키보드', 120000, 150);

-- 관리자 계정 생성
INSERT INTO users (username, email, password, status) VALUES
    ('admin', 'admin@example.com', '$2a$10$hashedpassword', 'ACTIVE');

V4__add_indexes.sql - 인덱스 최적화

-- V4__add_indexes.sql
-- 자주 조회되는 컬럼에 인덱스 추가
CREATE INDEX idx_users_email     ON users(email);
CREATE INDEX idx_users_status    ON users(status);
CREATE INDEX idx_orders_user_id  ON orders(user_id);
CREATE INDEX idx_orders_status   ON orders(status);
CREATE INDEX idx_orders_created  ON orders(created_at DESC);

-- 복합 인덱스
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

5. Java 기반 마이그레이션

SQL만으로 처리하기 어려운 복잡한 데이터 변환은 Java 마이그레이션으로 구현합니다.

BaseJavaMigration 구현

package db.migration;

import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;

import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

/**
 * V5__MigrateUserData
 * 기존 users 테이블의 full_name 컬럼을 first_name, last_name으로 분리
 */
public class V5__MigrateUserData extends BaseJavaMigration {

    @Override
    public void migrate(Context context) throws Exception {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(
            new SingleConnectionDataSource(context.getConnection(), true)
        );

        // 1단계: full_name 컬럼이 있는 경우에만 마이그레이션 실행
        List<String[]> users = new ArrayList<>();

        try (Statement stmt = context.getConnection().createStatement();
             ResultSet rs = stmt.executeQuery(
                 "SELECT id, full_name FROM users WHERE full_name IS NOT NULL")) {
            while (rs.next()) {
                long id = rs.getLong("id");
                String fullName = rs.getString("full_name");
                String[] parts = fullName.split(" ", 2);
                users.add(new String[]{
                    String.valueOf(id),
                    parts[0],
                    parts.length > 1 ? parts[1] : ""
                });
            }
        }

        // 2단계: 분리된 이름 저장
        for (String[] user : users) {
            jdbcTemplate.update(
                "UPDATE users SET first_name = ?, last_name = ? WHERE id = ?",
                user[1], user[2], Long.parseLong(user[0])
            );
        }

        // 3단계: 마이그레이션 결과 로깅
        System.out.printf("Migrated %d user records%n", users.size());
    }
}

Spring Bean 주입이 필요한 경우

package db.migration;

import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class V6__EncryptSensitiveData extends BaseJavaMigration {

    // 주의: Spring Bean 주입을 위해 @Component 필요
    // FlywayAutoConfiguration에서 JavaMigration Bean을 자동 인식
    @Autowired
    private EncryptionService encryptionService;

    @Override
    public void migrate(Context context) throws Exception {
        // encryptionService 사용 가능
        var jdbcTemplate = new org.springframework.jdbc.core.JdbcTemplate(
            new org.springframework.jdbc.datasource.SingleConnectionDataSource(
                context.getConnection(), true)
        );

        jdbcTemplate.query(
            "SELECT id, phone_number FROM users WHERE phone_number IS NOT NULL",
            (rs) -> {
                long id = rs.getLong("id");
                String phone = rs.getString("phone_number");
                String encrypted = encryptionService.encrypt(phone);
                jdbcTemplate.update(
                    "UPDATE users SET phone_number = ? WHERE id = ?",
                    encrypted, id
                );
            }
        );
    }
}

6. 멀티 환경 설정

Spring Profile 별 Flyway 설정

# application.yml (공통)
spring:
  flyway:
    enabled: true
    locations: classpath:db/migration

---
# application-dev.yml
spring:
  config:
    activate:
      on-profile: dev
  flyway:
    locations:
      - classpath:db/migration
      - classpath:db/migration/dev
    clean-on-validation-error: true # 개발 환경에서만 허용

---
# application-test.yml
spring:
  config:
    activate:
      on-profile: test
  flyway:
    locations:
      - classpath:db/migration
      - classpath:db/migration/test
    clean-disabled: false # 테스트 전 DB 초기화 허용

---
# application-prod.yml
spring:
  config:
    activate:
      on-profile: prod
  flyway:
    locations: classpath:db/migration
    clean-disabled: true # 운영 환경에서 clean 명령 비활성화
    out-of-order: false
    validate-on-migrate: true

벤더별 마이그레이션 분기

spring:
  flyway:
    locations:
      - classpath:db/migration
      - classpath:db/migration/{vendor}
db/migration/
├── V1__create_tables.sql          # 공통
├── V2__add_indexes.sql            # 공통
├── postgresql/
│   └── V3__add_pg_specific.sql   # PostgreSQL 전용
└── mysql/
    └── V3__add_mysql_specific.sql # MySQL 전용

Java Config를 통한 세밀한 설정

@Configuration
public class FlywayConfig {

    @Bean
    public FlywayMigrationStrategy migrationStrategy() {
        return flyway -> {
            // 마이그레이션 전 커스텀 로직
            System.out.println("Starting Flyway migration...");
            flyway.migrate();
            System.out.println("Flyway migration completed.");
        };
    }

    @Bean
    @Profile("!prod")
    public FlywayMigrationStrategy devMigrationStrategy() {
        return flyway -> {
            // 개발 환경: 스키마 초기화 후 재실행
            flyway.clean();
            flyway.migrate();
        };
    }
}

7. 테스트 전략

@FlywayTest 어노테이션 활용

<!-- flywaydb-test 라이브러리 -->
<dependency>
    <groupId>org.flywaydb.flyway-test-extensions</groupId>
    <artifactId>flyway-spring-test</artifactId>
    <version>9.5.0</version>
    <scope>test</scope>
</dependency>
@SpringBootTest
@FlywayTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    @FlywayTest(locationsForMigrate = {"classpath:db/migration/test"})
    void testUserCreation() {
        User user = new User("testuser", "test@example.com");
        User saved = userRepository.save(user);
        assertThat(saved.getId()).isNotNull();
    }
}

H2 인메모리 DB 테스트

# application-test.yml
spring:
  datasource:
    url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    driver-class-name: org.h2.Driver
    username: sa
    password:
  flyway:
    enabled: true
    locations: classpath:db/migration
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ActiveProfiles("test")
class ProductRepositoryTest {

    @Autowired
    private ProductRepository productRepository;

    @Test
    void findByStatus_returnsActiveProducts() {
        List<Product> products = productRepository.findByStatus("ACTIVE");
        assertThat(products).isNotEmpty();
    }
}

TestContainers + Flyway (권장)

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
@SpringBootTest
@Testcontainers
class IntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
            .withDatabaseName("testdb")
            .withUsername("testuser")
            .withPassword("testpass");

    @DynamicPropertySource
    static void setProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private UserRepository userRepository;

    @Test
    void flywayMigration_appliesAllScripts() {
        // Flyway가 적용된 실제 PostgreSQL 컨테이너에서 테스트
        assertThat(userRepository.count()).isGreaterThanOrEqualTo(0);
    }
}

8. 운영 환경 주의사항

컬럼 삭제 3단계 전략

운영 중인 시스템에서 컬럼을 즉시 삭제하면 배포 중 애플리케이션 오류가 발생할 수 있습니다. 3단계 접근법을 권장합니다.

1단계: 컬럼 NULL 허용으로 변경 (배포 N)

-- V10__make_old_column_nullable.sql
ALTER TABLE users ALTER COLUMN old_field DROP NOT NULL;

2단계: 애플리케이션 코드에서 해당 컬럼 참조 제거 (배포 N+1)

  • Java 엔티티, 쿼리, 리포지토리에서 참조 삭제

3단계: 컬럼 실제 삭제 (배포 N+2)

-- V12__drop_old_column.sql
ALTER TABLE users DROP COLUMN IF EXISTS old_field;

PostgreSQL 대용량 인덱스 생성 전략

-- V15__add_large_index.sql
-- CONCURRENTLY 옵션: 테이블 잠금 없이 인덱스 생성 (운영 환경 권장)
-- 주의: Flyway 트랜잭션 내에서는 CONCURRENTLY 사용 불가
-- Java 마이그레이션이나 별도 스크립트로 실행 필요
CREATE INDEX CONCURRENTLY idx_orders_large ON orders(created_at, status);

Flyway 트랜잭션 외부에서 실행하기 위한 Java 마이그레이션:

public class V15__AddLargeIndexConcurrently extends BaseJavaMigration {

    @Override
    public boolean canExecuteInTransaction() {
        return false; // 트랜잭션 외부 실행
    }

    @Override
    public void migrate(Context context) throws Exception {
        try (Statement stmt = context.getConnection().createStatement()) {
            stmt.execute(
                "CREATE INDEX CONCURRENTLY IF NOT EXISTS " +
                "idx_orders_large ON orders(created_at, status)"
            );
        }
    }
}

대용량 테이블 마이그레이션 전략

-- V16__migrate_large_table.sql
-- 1. 새 컬럼 추가 (NULL 허용)
ALTER TABLE orders ADD COLUMN new_status_code INTEGER;

-- 2. 배치 업데이트 (한 번에 1000건씩)
-- 이 방식은 Flyway SQL에서 직접 사용 어려우므로 Java 마이그레이션 권장
public class V16__BatchMigrateLargeTable extends BaseJavaMigration {

    private static final int BATCH_SIZE = 1000;

    @Override
    public void migrate(Context context) throws Exception {
        JdbcTemplate jdbc = new JdbcTemplate(
            new SingleConnectionDataSource(context.getConnection(), true)
        );

        long maxId = jdbc.queryForObject("SELECT MAX(id) FROM orders", Long.class);
        long processedCount = 0;

        for (long offset = 0; offset <= maxId; offset += BATCH_SIZE) {
            final long batchOffset = offset;
            int updated = jdbc.update(
                "UPDATE orders SET new_status_code = CASE status " +
                "WHEN 'PENDING' THEN 1 WHEN 'COMPLETED' THEN 2 ELSE 0 END " +
                "WHERE id > ? AND id <= ? AND new_status_code IS NULL",
                batchOffset, batchOffset + BATCH_SIZE
            );
            processedCount += updated;
            System.out.printf("Batch progress: %d records updated%n", processedCount);
        }
    }
}

flyway repair 명령

마이그레이션 실패 후 히스토리를 복구할 때 사용합니다.

# Maven
./mvnw flyway:repair

# Gradle
./gradlew flywayRepair

# CLI
flyway -url=jdbc:postgresql://localhost:5432/mydb \
       -user=myuser \
       -password=mypassword \
       repair
// 애플리케이션 코드에서 repair 실행
@Autowired
private Flyway flyway;

public void repairMigration() {
    flyway.repair();
}

9. CI/CD 통합

GitHub Actions에서 Flyway 검증

# .github/workflows/flyway-validate.yml
name: Flyway Validation

on:
  pull_request:
    paths:
      - 'src/main/resources/db/migration/**'
      - 'src/main/java/db/migration/**'

jobs:
  flyway-validate:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpass
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Validate Flyway migrations
        run: |
          ./mvnw flyway:validate \
            -Dflyway.url=jdbc:postgresql://localhost:5432/testdb \
            -Dflyway.user=testuser \
            -Dflyway.password=testpass

      - name: Run migration
        run: |
          ./mvnw flyway:migrate \
            -Dflyway.url=jdbc:postgresql://localhost:5432/testdb \
            -Dflyway.user=testuser \
            -Dflyway.password=testpass

      - name: Verify migration info
        run: |
          ./mvnw flyway:info \
            -Dflyway.url=jdbc:postgresql://localhost:5432/testdb \
            -Dflyway.user=testuser \
            -Dflyway.password=testpass

프로덕션 배포 전 검증 스크립트

#!/bin/bash
# scripts/pre-deploy-flyway-check.sh

set -e

echo "Running Flyway validation against production database..."

./mvnw flyway:validate \
  -Dflyway.url="${PROD_DB_URL}" \
  -Dflyway.user="${PROD_DB_USER}" \
  -Dflyway.password="${PROD_DB_PASSWORD}" \
  -Dflyway.cleanDisabled=true

echo "Flyway validation passed. Safe to deploy."
# Docker Compose 개발 환경
version: '3.8'
services:
  app:
    build: .
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/appdb
      SPRING_FLYWAY_ENABLED: 'true'
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:15
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: apppass
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U appuser -d appdb']
      interval: 5s
      timeout: 5s
      retries: 5

10. 자주 발생하는 문제와 해결책

체크섬 불일치 오류

ERROR: Validate failed: Migrations have failed validation
Migration checksum mismatch for migration version 3

원인: 이미 적용된 마이그레이션 파일을 수정한 경우

해결책:

# 수정된 파일의 체크섬으로 히스토리 업데이트
./mvnw flyway:repair

또는 새로운 마이그레이션 파일로 수정 내용 적용:

-- V3_1__fix_incorrect_v3_migration.sql
-- V3의 잘못된 부분을 수정하는 새 마이그레이션
ALTER TABLE users MODIFY COLUMN email VARCHAR(500);

베이스라인 설정 방법

기존 DB에 Flyway를 처음 도입할 때:

spring:
  flyway:
    baseline-on-migrate: true
    baseline-version: 1 # 현재 상태를 V1로 베이스라인 설정
    baseline-description: 'Initial baseline'
# CLI로 베이스라인 설정
flyway -url=jdbc:postgresql://localhost:5432/mydb \
       -user=myuser \
       -password=mypassword \
       baseline

퀴즈: Flyway 지식 점검

Q1. Flyway의 flyway_schema_history 테이블은 어떤 역할을 하나요?

정답: 마이그레이션 실행 이력을 추적하는 메타데이터 테이블입니다.

설명: flyway_schema_history 테이블은 적용된 각 마이그레이션 스크립트의 버전, 파일명, 체크섬(파일 변경 감지용), 실행 시각, 성공 여부 등을 저장합니다. Flyway는 이 테이블을 기반으로 어떤 마이그레이션이 이미 실행되었는지 파악하고, 새로 추가된 스크립트만 순서대로 실행합니다. 체크섬을 이용해 기 적용된 스크립트의 변경도 감지합니다.

Q2. baseline-on-migrate 옵션은 언제 사용하나요?

정답: 이미 운영 중인 기존 데이터베이스에 Flyway를 처음 도입할 때 사용합니다.

설명: 기존 DB에 flyway_schema_history 테이블이 없는 상태에서 Flyway를 처음 적용하면 오류가 발생할 수 있습니다. baseline-on-migrate: true로 설정하면 첫 실행 시 현재 DB 상태를 베이스라인으로 설정하고, 이후의 마이그레이션만 적용합니다. 단, 이미 Flyway가 관리 중인 DB에서는 사용하지 않는 것이 좋습니다.

Q3. Repeatable Migration(R__ 접두사)은 어떤 경우에 사용하나요?

정답: 뷰(View), 저장 프로시저(Stored Procedure), 함수처럼 내용이 변경될 때마다 재실행이 필요한 SQL 객체 관리에 사용합니다.

설명: Versioned Migration은 한 번만 실행되지만, Repeatable Migration은 파일의 체크섬이 변경될 때마다 재실행됩니다. 따라서 R__create_views.sql에 뷰 정의를 넣으면, 뷰 내용이 변경될 때 파일만 수정하면 자동으로 재적용됩니다. 단, Repeatable Migration은 항상 Versioned Migration이 모두 실행된 후에 실행됩니다.

Q4. 운영 환경에서 대용량 테이블의 인덱스를 추가할 때 Flyway 트랜잭션 처리를 어떻게 해야 하나요?

정답: canExecuteInTransaction() 메서드를 false로 오버라이드한 Java 마이그레이션을 사용하고, PostgreSQL의 CREATE INDEX CONCURRENTLY를 활용합니다.

설명: PostgreSQL의 CREATE INDEX CONCURRENTLY는 테이블 잠금 없이 인덱스를 생성하지만, 트랜잭션 내에서는 사용할 수 없습니다. Java 마이그레이션에서 canExecuteInTransaction()false로 반환하면 Flyway가 해당 마이그레이션을 트랜잭션 외부에서 실행합니다. 이를 통해 운영 중 서비스 중단 없이 안전하게 인덱스를 생성할 수 있습니다.

Q5. 이미 적용된 마이그레이션 파일을 수정했을 때 "checksum mismatch" 오류가 발생하면 어떻게 해결하나요?

정답: flyway repair 명령을 실행하거나, 새로운 버전의 마이그레이션 파일을 추가합니다.

설명: Flyway는 이미 적용된 마이그레이션 파일이 변경되면 체크섬 불일치 오류를 발생시킵니다. 이는 DB 일관성을 보호하기 위한 안전장치입니다. 해결 방법은 두 가지입니다. 첫째, flyway repair로 히스토리 테이블의 체크섬을 현재 파일과 동기화합니다(오직 실수로 파일을 수정한 개발 환경에서만 권장). 둘째, 절대 기존 파일을 수정하지 않고, 수정 내용을 담은 새로운 버전의 마이그레이션 파일을 추가합니다(운영 환경 필수).