Skip to content

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

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

1. Flyway란?

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

Flyway vs Liquibase 비교

| 항목 | Flyway | Liquibase |

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

| 마이그레이션 형식 | SQL 또는 Java | XML, 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 -->

<!-- MySQL 사용 시 추가 -->

<!-- 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;

/**

* 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;

@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 라이브러리 -->

@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 (권장)

@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 지식 점검

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

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

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

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

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

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

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

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

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

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

현재 단락 (1/484)

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

작성 글자: 0원문 글자: 14,349작성 단락: 0/484