Spring Boot + Flyway

왜 DB 마이그레이션 운영 설계가 필요한가

애플리케이션 코드는 Git으로 버전 관리하면서, 데이터베이스 스키마는 수동 ALTER를 실행하고 있다면 운영 장애는 시간문제입니다. Flyway는 SQL 스크립트를 버전별로 관리하고, 애플리케이션 기동 시 자동으로 미적용 마이그레이션을 순서대로 실행해주는 도구입니다. Spring Boot는 Flyway를 일급(first-class) 통합 대상으로 지원합니다.

이 글에서는 Flyway 공식 문서와 Spring Boot 공식 문서를 기반으로, 실무에서 자주 발생하는 마이그레이션 운영 실수와 이를 방지하는 설계 패턴을 다룹니다.

Spring Boot + Flyway 마이그레이션 실행 흐름 도식
Spring Boot 기동 시 Flyway 마이그레이션 실행 흐름과 파일 유형별 동작

1. Flyway 핵심 개념: Versioned vs Repeatable 마이그레이션

Versioned 마이그레이션 — 정확히 한 번 실행

Flyway 공식 문서에 따르면, Versioned 마이그레이션은 대상 데이터베이스에 순서대로 정확히 한 번 적용됩니다. flyway_schema_history 테이블이 어떤 버전이 적용되었는지 추적하고, 체크섬(checksum)을 저장하여 스크립트가 변경되지 않았는지 검증합니다.

-- 파일명: V001__create_user_table.sql
CREATE TABLE user (
    id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 파일명: V002__add_phone_column.sql
ALTER TABLE user ADD COLUMN phone VARCHAR(20) DEFAULT NULL;

네이밍 규칙: V<VERSION>__<DESCRIPTION>.sql 형식입니다. VERSION은 숫자(도트/언더스코어 구분 가능), 언더스코어 두 개(__)가 버전과 설명을 구분합니다.

공식 문서 권고: “이미 다운스트림 환경에 적용된 Versioned 마이그레이션은 수정하지 마세요. 변경이 필요하면 새 Versioned 마이그레이션을 만들어 roll forward 하는 것이 모범 사례입니다.”

Repeatable 마이그레이션 — 체크섬이 바뀔 때마다 재실행

Repeatable 마이그레이션은 버전 번호가 없고, 체크섬이 변경될 때마다 다시 적용됩니다. 공식 문서는 다음과 같은 용도를 권장합니다:

  • View, Stored Procedure, Function의 CREATE OR REPLACE
  • 참조 데이터(reference data) 벌크 재삽입
-- 파일명: R__create_active_users_view.sql
CREATE OR REPLACE VIEW active_users AS
SELECT id, email, name
FROM user
WHERE deleted_at IS NULL;

Repeatable 마이그레이션은 하나의 마이그레이션 실행에서 모든 pending Versioned 마이그레이션이 적용된 후 마지막에 실행되며, description(즉, 알파벳) 순으로 정렬됩니다.

비교 테이블

구분 Versioned Repeatable
파일 접두사 V R
실행 횟수 정확히 1회 체크섬 변경 시마다
실행 순서 버전 번호 오름차순 Versioned 이후, 알파벳순
주 용도 DDL(CREATE, ALTER), DML View/Procedure 재생성, 참조 데이터
수정 가능 적용 후 수정 금지(roll forward) 자유롭게 수정 가능

2. Spring Boot에서 Flyway 통합 설정

의존성과 기본 동작

Spring Boot 공식 문서에 따르면, org.flywaydb:flyway-core를 classpath에 추가하면 자동 구성(auto-configuration)이 활성화됩니다. MySQL을 사용하는 경우 org.flywaydb:flyway-mysql 모듈이 추가로 필요합니다.

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

기본 마이그레이션 위치는 classpath:db/migration이며, spring.flyway.locations으로 변경할 수 있습니다.

# application.yml
spring:
  flyway:
    locations: "classpath:db/migration"
    baseline-on-migrate: true      # 기존 DB에 Flyway를 처음 도입할 때
    baseline-version: 0            # 베이스라인 기준 버전

별도 DataSource 사용

Spring Boot는 기본적으로 @Primary DataSource를 Flyway에 주입합니다. 마이그레이션 전용 DataSource가 필요하면 @FlywayDataSource로 별도 빈을 등록하거나, spring.flyway.url/spring.flyway.user/spring.flyway.password를 설정해 Flyway 전용 연결을 사용할 수 있습니다. 이는 마이그레이션에 DDL 권한이 필요하지만 애플리케이션에는 DML 권한만 부여하고 싶을 때 유용합니다.

# 마이그레이션 전용 계정 (DDL 권한 포함)
spring:
  flyway:
    url: jdbc:mysql://db-host:3306/myapp
    user: flyway_admin
    password: ${FLYWAY_DB_PASSWORD}

# 애플리케이션 DataSource (DML만)
  datasource:
    url: jdbc:mysql://db-host:3306/myapp
    username: app_user
    password: ${APP_DB_PASSWORD}

프로파일별 마이그레이션

Spring Boot 공식 문서는 프로파일별 마이그레이션 경로 분리를 지원합니다. 예를 들어 application-dev.yml에서 테스트용 시드 데이터를 추가 위치로 지정할 수 있습니다:

# application-dev.yml
spring:
  flyway:
    locations: "classpath:db/migration,classpath:db/seed"

db/seed에 테스트 데이터 삽입 스크립트를 넣으면 개발/테스트 환경에서만 실행됩니다.

3. Schema History 테이블과 운영 중 장애 대응

flyway_schema_history의 역할

Flyway는 스키마에 flyway_schema_history 테이블을 자동 생성합니다. 공식 문서에 따르면, 이 테이블은 “스키마에 수행된 모든 변경의 완전한 감사 추적(complete audit trail)”이며, 마이그레이션 체크섬과 성공 여부를 기록합니다.

-- 스키마 히스토리 조회
SELECT installed_rank, version, description, type, checksum, success
FROM flyway_schema_history
ORDER BY installed_rank;

+----------------+---------+------------------------+------+-----------+---------+
| installed_rank | version | description            | type | checksum  | success |
+----------------+---------+------------------------+------+-----------+---------+
|              1 | 1       | create user table      | SQL  | 123456789 |       1 |
|              2 | 2       | add phone column       | SQL  | 987654321 |       1 |
|              3 | NULL    | create active users vi | SQL  | 555111222 |       1 |
+----------------+---------+------------------------+------+-----------+---------+

마이그레이션 상태(State) 이해

공식 문서는 다음과 같은 주요 상태를 정의합니다:

상태 의미 대응
Success 정상 적용됨 정상 상태
Pending 아직 적용되지 않음 다음 migrate 시 실행됨
Failed 실행 중 에러 발생 repair 후 재실행 필요
Missing 히스토리에는 있으나 파일을 찾을 수 없음 파일 복구 또는 repair
Out of Order 순서를 건너뛰고 적용됨 결과 검증 필요
Baseline 베이스라인으로 지정된 기준점 이 버전 이전은 무시됨

Failed 마이그레이션 복구 절차

마이그레이션이 실패하면 Flyway는 가능한 경우 자동 롤백합니다. 그러나 MySQL은 DDL에 대해 트랜잭션 롤백을 지원하지 않습니다(DDL은 암시적 커밋). 따라서 MySQL에서 마이그레이션이 중간에 실패하면 부분적으로 적용된 상태가 됩니다.

복구 절차:

  1. 실패한 마이그레이션이 남긴 부분 변경을 수동으로 확인하고 롤백합니다.
  2. flyway repair를 실행하여 히스토리 테이블에서 실패 기록을 제거합니다.
  3. 마이그레이션 스크립트를 수정하거나 새 스크립트를 작성합니다.
  4. flyway migrate를 다시 실행합니다.
# CLI에서 repair 실행
flyway -url=jdbc:mysql://db-host:3306/myapp repair

# Spring Boot에서는 FlywayMigrationStrategy 빈으로 커스터마이즈 가능
@Bean
FlywayMigrationStrategy repairStrategy() {
    return flyway -> {
        flyway.repair();
        flyway.migrate();
    };
}

실무 팁: MySQL을 사용하는 프로젝트에서는 하나의 마이그레이션 파일에 DDL을 가능한 한 하나만 포함하세요. 여러 DDL이 포함된 스크립트가 중간에 실패하면, 어디까지 적용되었는지 파악하기 어렵습니다.

4. 무중단 마이그레이션을 위한 설계 패턴

Kubernetes 환경에서 Rolling Update를 사용하면, 새 버전 Pod와 이전 버전 Pod가 동시에 같은 DB를 바라봅니다. 마이그레이션이 이전 버전과 호환되지 않으면 장애가 발생합니다.

패턴 1: Expand-and-Contract (확장 후 축소)

컬럼 이름 변경처럼 비호환 변경이 필요할 때, 한 번에 바꾸지 말고 3단계로 나눕니다:

-- V003__add_new_column.sql (Expand)
ALTER TABLE user ADD COLUMN full_name VARCHAR(200);
UPDATE user SET full_name = name WHERE full_name IS NULL;

-- 애플리케이션 배포: 새 코드는 full_name을 사용, 이전 코드는 name을 사용
-- 두 컬럼 모두 존재하므로 양쪽 모두 정상 동작

-- V004__drop_old_column.sql (Contract — 이전 버전 Pod가 완전히 제거된 후)
ALTER TABLE user DROP COLUMN name;

V003과 V004 사이에 반드시 이전 버전 Pod가 모두 종료된 것을 확인한 후 V004를 배포합니다.

패턴 2: 인덱스는 별도 마이그레이션으로 분리

대용량 테이블에 인덱스를 추가하면 MySQL에서 긴 락이 발생할 수 있습니다. 인덱스 생성은 별도 마이그레이션 파일로 분리하고, MySQL 8.0+에서는 Online DDL(ALGORITHM=INPLACE, LOCK=NONE)을 명시합니다:

-- V005__add_email_index.sql
ALTER TABLE user ADD INDEX idx_email (email) ALGORITHM=INPLACE, LOCK=NONE;

패턴 3: baseline-on-migrate로 기존 DB에 Flyway 도입

이미 스키마가 존재하는 운영 DB에 Flyway를 처음 도입할 때, baseline-on-migrate: true를 설정하면 Flyway가 현재 상태를 기준점(Baseline)으로 잡고 이후 마이그레이션만 적용합니다.

spring:
  flyway:
    baseline-on-migrate: true
    baseline-version: 10    # V010 이하는 이미 적용된 것으로 간주

5. 실전 체크리스트: 안전한 Flyway 운영

# 체크 항목 이유
1 적용된 마이그레이션 파일 수정 금지 체크섬 불일치로 기동 실패
2 MySQL DDL은 파일당 1개 DDL 트랜잭션 미지원으로 부분 실패 위험
3 비호환 변경은 Expand-Contract 패턴 Rolling Update 중 이전 버전 Pod 장애 방지
4 대형 테이블 인덱스는 LOCK=NONE 명시 테이블 락으로 서비스 중단 방지
5 마이그레이션 전용 DB 계정 분리 최소 권한 원칙(DDL ↔ DML 분리)
6 CI에서 flyway validate 실행 체크섬/순서 이상을 배포 전에 탐지
7 운영 배포 전 staging에서 마이그레이션 검증 데이터 볼륨 차이로 인한 예상 외 락/타임아웃
8 spring.flyway.enabled=false로 비상시 비활성화 마이그레이션 문제 시 애플리케이션 기동 차단 방지

6. 자주 발생하는 실수와 해결

실수 1: 이미 적용된 마이그레이션 파일을 수정

증상: 애플리케이션 기동 시 Migration checksum mismatch 에러로 시작 실패

원인: flyway_schema_history에 저장된 체크섬과 현재 파일 체크섬이 다름

해결: 파일을 원래대로 복원하거나, 의도적 변경이면 flyway repair로 체크섬을 갱신

실수 2: 팀원 간 버전 번호 충돌

증상: 두 개발자가 동시에 V005__를 작성하여 merge 후 충돌

해결: 버전 번호에 타임스탬프를 사용합니다. Flyway 공식 문서도 “Desktop에서는 기본적으로 타임스탬프를 버전에 포함하여 충돌을 방지한다”고 안내합니다.

-- 타임스탬프 기반 버전 네이밍
V20260219_0600__add_user_status.sql
V20260219_0930__add_order_table.sql

실수 3: Spring Boot 테스트에서 마이그레이션 충돌

증상: 테스트마다 DB 상태가 누적되어 예측 불가능한 테스트 실패

해결: spring.flyway.clean-disabled=false@Sql로 테스트 격리. 단, 운영에서는 절대 clean을 활성화하지 마세요—전체 스키마가 삭제됩니다.

마무리

Flyway + Spring Boot 조합은 설정 자체는 간단하지만, 운영에서의 안전성은 마이그레이션 파일 설계에 달려 있습니다. “적용된 파일 수정 금지” 원칙을 지키고, MySQL DDL의 트랜잭션 한계를 인식하며, Kubernetes 롤링 업데이트를 고려한 Expand-Contract 패턴을 적용하면 무중단 스키마 변경이 가능해집니다.

참고 자료

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