Flyway - 데이터베이스 마이그레이션의 모든 것
TL;DR
- Flyway - 데이터베이스 마이그레이션의 모든 것의 핵심 개념을 빠르게 파악할 수 있다.
- 배경과 이유를 통해 왜 필요한지 맥락을 이해할 수 있다.
- 특징과 상세 내용을 통해 실무 적용 포인트를 확인할 수 있다.
1. 개념
Flyway - 데이터베이스 마이그레이션의 모든 것의 핵심 정의와 문제 공간을 간단히 정리한다.
2. 배경
이 주제가 등장한 기술적·조직적 배경과 기존 접근의 한계를 설명한다.
3. 이유
왜 지금 이 방식을 채택해야 하는지, 기대 효과와 트레이드오프를 함께 정리한다.
4. 특징
핵심 동작 방식, 장단점, 적용 시 주의점을 빠르게 훑을 수 있도록 요약한다.
5. 상세 내용
Flyway - 데이터베이스 마이그레이션의 모든 것
작성일: 2026-02-27 카테고리: Backend / Database / Migration 포함 내용: Flyway, Database Migration, Schema Versioning, flyway_schema_history, Versioned Migration, Repeatable Migration, Undo Migration, Spring Boot 통합, Liquibase 비교, Expand-Contract Pattern, Zero-Downtime Migration, CI/CD
1. 탄생 배경과 역사
1.1 도구 없는 데이터베이스 변경 = 재앙
┌─────────────────────────────────────────────────────────────────┐
│ 도구 없이 DB 스키마를 관리하면 생기는 일 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 시나리오: 4명의 개발자가 동시에 스키마를 변경 │
│ │
│ 개발자 A: ALTER TABLE users ADD COLUMN phone VARCHAR(20); │
│ 개발자 B: ALTER TABLE users ADD COLUMN address TEXT; │
│ 개발자 C: CREATE TABLE orders (...); │
│ 개발자 D: ALTER TABLE users DROP COLUMN nickname; │
│ │
│ 문제점: │
│ ├── 누가 먼저 적용해야 하는지 아무도 모름 │
│ ├── 로컬 DB와 개발 서버 DB가 서로 다른 상태 │
│ ├── "내 컴퓨터에서는 되는데?" 반복 │
│ ├── 스테이징에 배포하면 에러 폭발 │
│ └── 프로덕션 배포 = 기도하고 실행하기 │
│ │
│ 실제 사고 사례: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ TSB Bank (2018년 영국) │ │
│ │ ├── 데이터베이스 마이그레이션 실패로 서비스 장애 │ │
│ │ ├── 190만 고객이 계좌 접근 불가 │ │
│ │ ├── 일부 고객이 타인 계좌 정보 열람 │ │
│ │ ├── 복구에 수 주 소요 │ │
│ │ └── CEO 사임 + £48.65M 벌금 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Experian 연구 (데이터 마이그레이션 프로젝트): │
│ ├── 64%가 예산 초과 │
│ ├── 46%만 일정 내 완료 │
│ └── 주요 원인: 체계적 도구/프로세스 부재 │
│ │
└─────────────────────────────────────────────────────────────────┘
1.2 Schema Drift 문제
┌─────────────────────────────────────────────────────────────────┐
│ Schema Drift = 스키마 표류 현상 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 정의: 동일해야 할 여러 환경의 DB 스키마가 서로 달라지는 현상 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 로컬 DB │ │ 개발 DB │ │ 스테이징 │ │ 프로덕션 │ │
│ │ │ │ │ │ DB │ │ DB │ │
│ ├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤ │
│ │ users │ │ users │ │ users │ │ users │ │
│ │ + phone │ │ + phone │ │ │ │ │ │
│ │ + addr │ │ │ │ + addr │ │ + nick │ │
│ │ orders ✓ │ │ orders ✗ │ │ orders ✓ │ │ orders ✗ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ↑ ↑ ↑ ↑ │
│ 4개 환경이 전부 다른 상태! = Schema Drift │
│ │
│ Schema Drift가 발생하는 원인: │
│ ├── 수동 SQL 실행 (DBA가 프로덕션에서 직접 ALTER TABLE) │
│ ├── 핫픽스로 인한 임시 변경이 다른 환경에 반영 안 됨 │
│ ├── ORM ddl-auto=update가 환경마다 다르게 동작 │
│ └── 마이그레이션 이력 관리 부재 │
│ │
│ 결과: │
│ ├── "이 컬럼이 왜 여기 있지?" (기원 불명) │
│ ├── 프로덕션에만 존재하는 인덱스 (누가 만들었는지 모름) │
│ ├── 리포팅 DB와 운영 DB의 스키마 불일치 │
│ └── 장애 복구 시 "원래 스키마가 뭐였지?" 혼란 │
│ │
└─────────────────────────────────────────────────────────────────┘
1.3 Flyway의 탄생
┌─────────────────────────────────────────────────────────────────┐
│ Flyway의 역사 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 이론적 토대: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 2003년: Martin Fowler │ │
│ │ "Evolutionary Database Design" 발표 │ │
│ │ → "DB 스키마도 코드처럼 점진적으로 진화해야 한다" │ │
│ │ │ │
│ │ 2006년: Scott Ambler & Pramod Sadalage │ │
│ │ "Refactoring Databases" 출간 │ │
│ │ → DB 리팩토링을 체계적 패턴으로 정리 │ │
│ │ │ │
│ │ 핵심 주장: │ │
│ │ "데이터베이스 변경도 버전 관리되어야 한다. │ │
│ │ 애플리케이션 코드와 동일한 수준의 관리가 필요하다." │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Flyway 연혁: │
│ ├── 2010년: Axel Fontaine이 Flyway 최초 공개 │
│ │ → "DB 스키마 버전 관리" 문제를 해결하겠다! │
│ │ → SQL 파일 기반의 단순한 접근 방식 채택 │
│ ├── 2015년: ThoughtWorks Technology Radar "Adopt" 등급 │
│ │ → 업계 최고 권위 기술 레이더에서 "도입 권장" │
│ ├── 2019년: Redgate가 Flyway 인수 (약 $10M 추정) │
│ │ → 데이터베이스 도구 전문 기업의 전폭적 지원 │
│ ├── 2020년+: Community / Teams / Enterprise 에디션 분리 │
│ └── 현재: JVM 생태계 DB 마이그레이션 사실상 표준 │
│ │
│ Flyway의 설계 철학: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ "Simplicity is the ultimate sophistication" │ │
│ │ - Leonardo da Vinci │ │
│ │ │ │
│ │ 1. SQL이 최고의 DDL 언어다 (SQL-first) │ │
│ │ 2. 마이그레이션은 순서가 있다 (Ordered) │ │
│ │ 3. 적용된 마이그레이션은 불변이다 (Immutable) │ │
│ │ 4. 단순함이 곧 강력함이다 (Simple > Complex) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2. 핵심 개념
2.1 마이그레이션의 세 가지 유형
┌─────────────────────────────────────────────────────────────────┐
│ Flyway 마이그레이션 유형 3가지 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Versioned Migration (버전 마이그레이션) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 파일명: V{버전}__{설명}.sql │ │
│ │ │ │
│ │ 예시: │ │
│ │ ├── V1__Create_users_table.sql │ │
│ │ ├── V2__Add_email_to_users.sql │ │
│ │ ├── V3__Create_orders_table.sql │ │
│ │ └── V1.1__Add_phone_to_users.sql │ │
│ │ │ │
│ │ 특징: │ │
│ │ ├── 딱 한 번만 적용됨 (once and only once) │ │
│ │ ├── 버전 순서대로 실행됨 │ │
│ │ ├── 적용 후 checksum으로 무결성 검증 │ │
│ │ └── 적용된 파일은 절대 수정하면 안 됨! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 2. Repeatable Migration (반복 마이그레이션) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 파일명: R__{설명}.sql │ │
│ │ │ │
│ │ 예시: │ │
│ │ ├── R__Create_user_view.sql │ │
│ │ ├── R__Update_stored_procedures.sql │ │
│ │ └── R__Refresh_materialized_views.sql │ │
│ │ │ │
│ │ 특징: │ │
│ │ ├── checksum이 변경될 때마다 재실행 │ │
│ │ ├── 버전 번호가 없음 (R__ 로 시작) │ │
│ │ ├── 모든 Versioned 마이그레이션 이후에 실행 │ │
│ │ ├── View, Stored Procedure 관리에 이상적 │ │
│ │ └── 항상 CREATE OR REPLACE 형태로 작성 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 3. Undo Migration (되돌리기 마이그레이션) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 파일명: U{버전}__{설명}.sql │ │
│ │ │ │
│ │ 예시: │ │
│ │ ├── U2__Add_email_to_users.sql ← V2의 역순 │ │
│ │ └── U3__Create_orders_table.sql ← V3의 역순 │ │
│ │ │ │
│ │ 특징: │ │
│ │ ├── ⚠️ Enterprise Edition 전용! │ │
│ │ ├── 해당 버전의 Versioned Migration을 되돌림 │ │
│ │ ├── Community Edition에서는 사용 불가 │ │
│ │ └── 대안: 새로운 Versioned Migration으로 롤백 로직 작성 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2.2 파일 명명 규칙 (Naming Convention)
┌─────────────────────────────────────────────────────────────────┐
│ 파일명 해부하기 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ V2.1__Add_email_column.sql │
│ │ │ │ │ │
│ │ │ │ └── 확장자: .sql │
│ │ │ │ │
│ │ │ └── 설명 (Description) │
│ │ │ ├── 언더스코어(_)로 단어 구분 │
│ │ │ └── 무엇을 하는지 명확하게 │
│ │ │ │
│ │ └── __ (이중 밑줄 = 구분자) │
│ │ └── 반드시 언더스코어 2개! 1개면 인식 안 됨 │
│ │ │
│ └── V + 버전 번호 │
│ ├── V1, V2, V3 (정수) │
│ ├── V1.1, V1.2 (소수점) │
│ ├── V20240101 (날짜 기반) │
│ └── V202401011430 (타임스탬프 기반) │
│ │
│ 실무 팁 - 타임스탬프 기반 권장: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 정수 방식의 문제: │ │
│ │ 개발자 A → V4__Add_phone.sql (월요일) │ │
│ │ 개발자 B → V4__Add_address.sql (화요일) │ │
│ │ → 충돌! 같은 버전 번호! │ │
│ │ │ │
│ │ 타임스탬프 방식: │ │
│ │ 개발자 A → V20260227143000__Add_phone.sql │ │
│ │ 개발자 B → V20260227150000__Add_address.sql │ │
│ │ → 충돌 가능성 거의 제로! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2.3 flyway_schema_history 테이블
┌─────────────────────────────────────────────────────────────────┐
│ flyway_schema_history = 마이그레이션 감사 로그 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Flyway가 자동으로 생성하는 메타데이터 테이블 │
│ → 어떤 마이그레이션이 언제, 누가, 얼마나 걸려서 적용됐는지 │
│ │
│ 테이블 구조: │
│ ┌──────────────────┬──────────┬──────────────────────────┐ │
│ │ 컬럼명 │ 타입 │ 설명 │ │
│ ├──────────────────┼──────────┼──────────────────────────┤ │
│ │ installed_rank │ INT │ 적용 순서 (1, 2, 3...) │ │
│ │ version │ VARCHAR │ 버전 번호 (1, 2, 2.1) │ │
│ │ description │ VARCHAR │ 설명 (Add_email 등) │ │
│ │ type │ VARCHAR │ SQL / JAVA / SPRING_JDBC │ │
│ │ script │ VARCHAR │ 파일명 │ │
│ │ checksum │ INT │ 파일 내용의 CRC32 해시 │ │
│ │ installed_by │ VARCHAR │ DB 접속 사용자명 │ │
│ │ installed_on │ TIMESTAMP│ 적용 시각 │ │
│ │ execution_time │ INT │ 실행 시간 (밀리초) │ │
│ │ success │ BOOLEAN │ 성공 여부 │ │
│ └──────────────────┴──────────┴──────────────────────────┘ │
│ │
│ 실제 데이터 예시: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ rank │ ver │ description │ checksum │ success │ │
│ ├──────┼──────┼──────────────────┼────────────┼───────────┤ │
│ │ 1 │ 1 │ Create users │ -12345678 │ true │ │
│ │ 2 │ 2 │ Add email column │ 98765432 │ true │ │
│ │ 3 │ 3 │ Create orders │ -55667788 │ true │ │
│ │ 4 │ 4 │ Add phone column │ 11223344 │ false │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 핵심 원칙: │
│ ├── 이 테이블은 불변 감사 로그 (Immutable Audit Trail) │
│ ├── 수동으로 절대 수정하지 말 것! (repair 명령어 사용) │
│ ├── checksum으로 파일 변조 감지 │
│ └── success=false인 항목은 repair로 정리 │
│ │
└─────────────────────────────────────────────────────────────────┘
2.4 마이그레이션 상태 (Migration States)
┌─────────────────────────────────────────────────────────────────┐
│ 마이그레이션 라이프사이클 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 파일 시스템에 존재 flyway_schema_history에 기록 │
│ │
│ ┌──────────┐ │
│ │ Pending │ → 파일은 있지만 아직 적용 안 됨 │
│ │ (대기) │ (history 테이블에 없음) │
│ └────┬─────┘ │
│ │ flyway migrate 실행 │
│ ▼ │
│ ┌──────────┐ │
│ │ Applied │ → 성공적으로 적용됨 │
│ │ (적용됨) │ (success=true) │
│ └──────────┘ │
│ │
│ ┌──────────┐ │
│ │ Failed │ → 실행 중 에러 발생 │
│ │ (실패) │ (success=false) │
│ └──────────┘ → repair 명령어로 정리 필요 │
│ │
│ ┌──────────────┐ │
│ │ Out of Order │ → 이미 높은 버전이 적용된 상태에서 │
│ │ (순서 이탈) │ 낮은 버전이 나중에 적용됨 │
│ └──────────────┘ → outOfOrder=true 설정 필요 │
│ │
│ ┌──────────────┐ │
│ │ Superseded │ → Repeatable Migration이 새 버전으로 │
│ │ (대체됨) │ 교체되어 이전 기록이 대체된 상태 │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
3. 주요 명령어
3.1 7가지 핵심 명령어
┌─────────────────────────────────────────────────────────────────┐
│ Flyway 핵심 명령어 7가지 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. migrate (마이그레이트) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 역할: Pending 상태의 마이그레이션을 순서대로 적용 │ │
│ │ 사용: flyway migrate │ │
│ │ │ │
│ │ 동작 흐름: │ │
│ │ 1) flyway_schema_history 테이블 확인 (없으면 생성) │ │
│ │ 2) 적용된 마이그레이션 목록 조회 │ │
│ │ 3) Pending 마이그레이션을 버전 순서대로 실행 │ │
│ │ 4) 각 마이그레이션의 checksum 기록 │ │
│ │ 5) 실행 결과 (성공/실패) 기록 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 2. clean (클린) ⚠️ 위험! │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 역할: 스키마의 모든 객체를 삭제 (테이블, 뷰, 인덱스) │ │
│ │ 사용: flyway clean │ │
│ │ │ │
│ │ ⚠️ 절대 프로덕션에서 실행하지 말 것! │ │
│ │ → Flyway 9.0+: cleanDisabled=true 기본값 │ │
│ │ → 개발/테스트 환경에서만 사용 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 3. info (인포) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 역할: 모든 마이그레이션의 상태를 보여줌 │ │
│ │ 사용: flyway info │ │
│ │ │ │
│ │ 출력 예시: │ │
│ │ +---------+-----+----------------+----------+ │ │
│ │ | Version | Desc| Type | State | │ │
│ │ +---------+-----+----------------+----------+ │ │
│ │ | 1 | Init| SQL | Applied | │ │
│ │ | 2 | Add | SQL | Applied | │ │
│ │ | 3 | Tbl | SQL | Pending | │ │
│ │ +---------+-----+----------------+----------+ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 4. validate (검증) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 역할: 적용된 마이그레이션과 로컬 파일의 일치 여부 확인 │ │
│ │ 사용: flyway validate │ │
│ │ │ │
│ │ 검증 항목: │ │
│ │ ├── checksum 일치 여부 (파일이 수정됐는지) │ │
│ │ ├── 누락된 마이그레이션 (파일이 삭제됐는지) │ │
│ │ └── 버전 순서 정합성 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 5. baseline (베이스라인) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 역할: 기존 DB에 Flyway를 도입할 때 시작점 설정 │ │
│ │ 사용: flyway baseline -baselineVersion=3 │ │
│ │ │ │
│ │ 시나리오: │ │
│ │ ├── 이미 운영 중인 DB에 Flyway를 처음 적용할 때 │ │
│ │ ├── V1~V3까지의 변경은 이미 수동 적용된 상태 │ │
│ │ ├── baselineVersion=3으로 설정하면 │ │
│ │ └── V4부터 Flyway가 관리 시작 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 6. repair (복구) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 역할: flyway_schema_history 테이블의 문제 해결 │ │
│ │ 사용: flyway repair │ │
│ │ │ │
│ │ 수행 작업: │ │
│ │ ├── 실패한 마이그레이션 기록 제거 (success=false) │ │
│ │ ├── checksum 재정렬 (파일 수정 후 동기화) │ │
│ │ └── 누락된 마이그레이션 정보 업데이트 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 7. undo (되돌리기) ⚠️ Enterprise 전용 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 역할: 마지막으로 적용된 마이그레이션을 되돌림 │ │
│ │ 사용: flyway undo │ │
│ │ │ │
│ │ Community Edition 대안: │ │
│ │ → 새로운 V 마이그레이션으로 롤백 SQL 작성 │ │
│ │ 예: V5__Rollback_add_phone.sql │ │
│ │ ALTER TABLE users DROP COLUMN phone; │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
3.2 명령어 실행 방법
┌─────────────────────────────────────────────────────────────────┐
│ Flyway 실행 방법 4가지 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. CLI (Command Line Interface) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ $ flyway -url=jdbc:postgresql://localhost/mydb \ │ │
│ │ -user=admin -password=secret migrate │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 2. Java API │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Flyway flyway = Flyway.configure() │ │
│ │ .dataSource(url, user, password) │ │
│ │ .load(); │ │
│ │ flyway.migrate(); │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 3. Spring Boot (자동 실행) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # application.yml │ │
│ │ spring: │ │
│ │ flyway: │ │
│ │ enabled: true ← 이것만으로 앱 시작 시 자동 실행 │ │
│ │ locations: classpath:db/migration │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 4. Gradle / Maven 플러그인 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # Gradle │ │
│ │ $ ./gradlew flywayMigrate │ │
│ │ │ │
│ │ # Maven │ │
│ │ $ mvn flyway:migrate │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4. 어떤 상황에서 효과적인가?
4.1 Flyway가 빛나는 상황
┌─────────────────────────────────────────────────────────────────┐
│ Flyway를 써야 하는 6가지 상황 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 팀 개발 (Team Development) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 여러 개발자가 동시에 스키마 변경 작업 │ │
│ │ → 각자 V 파일 생성 → merge 시 순서 자동 정렬 │ │
│ │ → "이거 적용했어?" 물어볼 필요 없음 │ │
│ │ → flyway info 한 번이면 현재 상태 파악 완료 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 2. CI/CD 파이프라인 통합 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Build → Test → flyway migrate → Deploy │ │
│ │ → 배포 시 자동으로 DB 스키마 업데이트 │ │
│ │ → 수동 SQL 실행 제로 │ │
│ │ → 롤백 시 어떤 버전으로 돌아가야 하는지 명확 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 3. 마이크로서비스 아키텍처 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 각 서비스가 자체 DB 스키마를 독립적으로 관리 │ │
│ │ ├── user-service → src/main/resources/db/migration/ │ │
│ │ ├── order-service → src/main/resources/db/migration/ │ │
│ │ └── payment-service→ src/main/resources/db/migration/ │ │
│ │ → 서비스별 독립적 스키마 진화 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 4. 규정 준수 / 감사 요구 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ flyway_schema_history = 불변 감사 추적 │ │
│ │ ├── 누가 언제 어떤 변경을 적용했는지 기록 │ │
│ │ ├── 금융, 의료, 공공 분야의 규제 충족 │ │
│ │ └── Git 이력 + Flyway 이력 = 완전한 변경 추적 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 5. 기존 데이터베이스 도입 (Legacy Adoption) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 이미 운영 중인 DB에 Flyway 도입 │ │
│ │ → baseline 명령어로 현재 상태를 기준점 설정 │ │
│ │ → 이후 변경부터 Flyway로 관리 │ │
│ │ → 점진적 도입 가능 (Big Bang 불필요) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 6. Blue-Green / Canary 배포 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 여러 환경의 DB 스키마를 동일하게 유지 │ │
│ │ ├── Blue 환경: V1~V10 적용 상태 │ │
│ │ ├── Green 환경: V1~V10 적용 + V11 Pending │ │
│ │ └── 전환 시 flyway migrate → V11 자동 적용 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4.2 Flyway가 적합하지 않은 상황
┌─────────────────────────────────────────────────────────────────┐
│ Flyway를 쓰지 않아도 되는 경우 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 빠른 프로토타이핑 / PoC │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ → 스키마가 하루에도 수십 번 바뀌는 초기 개발 │ │
│ │ → Hibernate ddl-auto=update가 더 편리 │ │
│ │ → 스키마가 안정화된 후 Flyway 도입 권장 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 2. 스키마 없는 데이터베이스 (NoSQL) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ → MongoDB, DynamoDB 등 Schema-less DB │ │
│ │ → 스키마 마이그레이션 자체가 불필요 │ │
│ │ → 다만 MongoDB도 Mongock 같은 도구가 있긴 함 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 3. 1인 개발 + 단순 프로젝트 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ → 스키마 변경이 거의 없는 소규모 프로젝트 │ │
│ │ → 오버엔지니어링이 될 수 있음 │ │
│ │ → 단, 프로덕션 배포 시에는 규모와 관계없이 권장 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5. 12가지 흔한 트러블 상황
5.1 Checksum Mismatch (체크섬 불일치)
┌─────────────────────────────────────────────────────────────────┐
│ 트러블 #1: Checksum Mismatch │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 상황: 이미 적용된 마이그레이션 파일을 수정했을 때 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ V2__Add_email.sql (원본, 이미 적용됨) │ │
│ │ ALTER TABLE users ADD COLUMN email VARCHAR(100); │ │
│ │ → checksum: 12345678 │ │
│ │ │ │
│ │ 개발자가 "아, 255로 바꿔야지" 하고 수정: │ │
│ │ ALTER TABLE users ADD COLUMN email VARCHAR(255); │ │
│ │ → checksum: 87654321 (달라짐!) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 에러 메시지: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Migration checksum mismatch for migration version 2 │ │
│ │ -> Applied to database : 12345678 │ │
│ │ -> Resolved locally : 87654321 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 해결: │
│ ├── 방법 1: 원본 파일로 되돌리고, V3로 변경 사항 작성 │
│ ├── 방법 2: flyway repair (checksum 재동기화, 주의 필요!) │
│ └── 원칙: 적용된 마이그레이션은 절대 수정하지 않는다! │
│ │
└─────────────────────────────────────────────────────────────────┘
5.2 Version Conflict (버전 충돌)
┌─────────────────────────────────────────────────────────────────┐
│ 트러블 #2: 동일 버전 번호 충돌 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 상황: 두 개발자가 같은 버전 번호로 마이그레이션 생성 │
│ │
│ 개발자 A (feature/user-phone 브랜치): │
│ └── V4__Add_phone_to_users.sql │
│ │
│ 개발자 B (feature/user-address 브랜치): │
│ └── V4__Add_address_to_users.sql │
│ │
│ merge 후: 같은 V4가 2개! → Flyway 에러 │
│ │
│ 에러 메시지: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Found more than one migration with version 4 │ │
│ │ Offenders: │ │
│ │ -> V4__Add_phone_to_users.sql │ │
│ │ -> V4__Add_address_to_users.sql │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 예방: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 타임스탬프 기반 버전 번호 사용! │ │
│ │ │ │
│ │ V20260227143000__Add_phone_to_users.sql (A) │ │
│ │ V20260227150000__Add_address_to_users.sql (B) │ │
│ │ │ │
│ │ → 시간이 다르면 절대 충돌 안 함 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5.3 Out-of-Order Migration (순서 이탈)
┌─────────────────────────────────────────────────────────────────┐
│ 트러블 #3: Out-of-Order Migration │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 상황: V5가 이미 적용된 상태에서 V4가 나중에 추가됨 │
│ │
│ 타임라인: │
│ 1) main 브랜치에 V3, V5 적용됨 │
│ 2) feature 브랜치에서 만든 V4가 뒤늦게 merge │
│ 3) Flyway가 V4를 발견 → 기본 동작: 무시 또는 에러! │
│ │
│ 해결: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # application.yml │ │
│ │ spring: │ │
│ │ flyway: │ │
│ │ out-of-order: true ← 순서 이탈 허용 │ │
│ │ │ │
│ │ # 또는 CLI │ │
│ │ flyway -outOfOrder=true migrate │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 주의: out-of-order를 항상 켜두면 │
│ → 의도치 않은 순서 이탈도 허용되므로 │
│ → 팀 규칙으로 관리하는 것이 중요 │
│ │
└─────────────────────────────────────────────────────────────────┘
5.4 Large Table ALTER Lock (대형 테이블 잠금)
┌─────────────────────────────────────────────────────────────────┐
│ 트러블 #4: ALTER TABLE이 프로덕션을 멈춤 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 상황: 수백만 행 테이블에 ALTER TABLE 실행 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ -- V10__Add_index_to_orders.sql │ │
│ │ ALTER TABLE orders ADD INDEX idx_user_id (user_id); │ │
│ │ │ │
│ │ orders 테이블: 5천만 행 │ │
│ │ → MySQL에서 이 작업: 테이블 락 발생! │ │
│ │ → 모든 INSERT/UPDATE 대기 → 서비스 장애 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 해결 전략: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ MySQL: │ │
│ │ ├── pt-online-schema-change (Percona Toolkit) │ │
│ │ ├── gh-ost (GitHub Online Schema Migration) │ │
│ │ └── ALTER ... ALGORITHM=INPLACE, LOCK=NONE; │ │
│ │ │ │
│ │ PostgreSQL: │ │
│ │ ├── CREATE INDEX CONCURRENTLY (비차단 인덱스 생성) │ │
│ │ └── ALTER TABLE ... SET NOT NULL 대신 │ │
│ │ CHECK constraint + NOT VALID → VALIDATE 분리 실행 │ │
│ │ │ │
│ │ 공통 원칙: │ │
│ │ → 대형 테이블 변경은 Flyway SQL 안에서 │ │
│ │ DB별 비차단(non-blocking) 전략 사용 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5.5 Failed Migration Dirty State (실패 후 오염 상태)
┌─────────────────────────────────────────────────────────────────┐
│ 트러블 #5: 마이그레이션 실패 후 DB 상태 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ DB별 트랜잭션 DDL 지원 차이: │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL: DDL도 트랜잭션 안에서 롤백 가능! │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ BEGIN; │ │ │
│ │ │ CREATE TABLE temp (...); -- 성공 │ │ │
│ │ │ ALTER TABLE users ADD ... ; -- 실패! │ │ │
│ │ │ ROLLBACK; ← temp 테이블도 롤백됨 │ │ │
│ │ │ → 깨끗한 상태 유지! │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ MySQL: DDL은 자동 커밋! 롤백 불가! │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ BEGIN; │ │ │
│ │ │ CREATE TABLE temp (...); -- 성공 → 자동 커밋! │ │ │
│ │ │ ALTER TABLE users ADD ... ; -- 실패! │ │ │
│ │ │ ROLLBACK; ← temp 테이블은 이미 커밋됨! │ │ │
│ │ │ → 절반만 적용된 오염 상태! │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ MySQL에서의 대응: │
│ ├── 마이그레이션 파일당 하나의 DDL만 작성 (원자성 확보) │
│ ├── 실패 시 flyway repair → 수동으로 잔여 DDL 정리 │
│ └── 테스트 환경에서 반드시 사전 실행 검증 │
│ │
└─────────────────────────────────────────────────────────────────┘
5.6 나머지 7가지 트러블
┌─────────────────────────────────────────────────────────────────┐
│ 트러블 #6~#12: 빠르게 짚어보기 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ #6. Baseline 설정 실수 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 잘못된 baselineVersion 설정 → 필요한 마이그레이션 건너뜀│ │
│ │ 예: V1~V5가 있는데 baselineVersion=3으로 설정 │ │
│ │ → V4, V5가 이미 적용된 것으로 간주되어 실행 안 됨 │ │
│ │ → 해결: baseline 전 반드시 flyway info로 상태 확인 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ #7. 데이터 마이그레이션 복잡성 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 스키마 변경과 데이터 변환을 같은 파일에 혼합 │ │
│ │ → 실패 시 어디까지 적용됐는지 파악 어려움 │ │
│ │ → 해결: 스키마 변경(V3)과 데이터 변환(V4) 분리 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ #8. Hibernate ddl-auto와 충돌 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ddl-auto=validate가 Flyway migrate보다 먼저 실행되면 │ │
│ │ → "테이블이 없어!" 에러 발생 │ │
│ │ │ │
│ │ 해결: │ │
│ │ spring.jpa.hibernate.ddl-auto=none │ │
│ │ spring.flyway.enabled=true │ │
│ │ → Flyway가 먼저 실행되고, Hibernate는 스키마 건드리지 X │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ #9. 환경 간 Schema Drift │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 개발/스테이징/프로덕션에 서로 다른 마이그레이션 적용 │ │
│ │ → 해결: 모든 환경에서 동일한 마이그레이션 파일 사용 │ │
│ │ → 환경별 차이는 Flyway placeholder로 처리 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ #10. Community Edition 롤백 한계 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Community: undo 명령어 없음 │ │
│ │ → 대안: "Forward-only migration" 전략 채택 │ │
│ │ → 롤백이 필요하면 새로운 V 파일로 역변경 작성 │ │
│ │ → 예: V6__Rollback_V5_changes.sql │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ #11. 인코딩 이슈 (UTF-8 BOM) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Windows 메모장이 UTF-8 BOM (0xEF, 0xBB, 0xBF) 삽입 │ │
│ │ → SQL 파서가 첫 문자 인식 실패 │ │
│ │ → "Incorrect syntax near '?'" 에러 │ │
│ │ → 해결: UTF-8 without BOM으로 저장 │ │
│ │ → IntelliJ: File → File Properties → UTF-8 (no BOM) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ #12. Placeholder 미해석 오류 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SQL 안에 ${schema_name} 같은 변수가 있을 때 │ │
│ │ → Flyway가 placeholder로 인식하지만 값이 없으면 에러 │ │
│ │ │ │
│ │ 해결: │ │
│ │ # application.yml │ │
│ │ spring: │ │
│ │ flyway: │ │
│ │ placeholders: │ │
│ │ schema_name: my_schema │ │
│ │ placeholder-replacement: false ← 또는 기능 끄기 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6. Flyway vs Liquibase 비교
6.1 핵심 차이점
┌─────────────────────────────────────────────────────────────────┐
│ Flyway vs Liquibase 상세 비교 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────┬──────────────────┬──────────────────┐ │
│ │ 항목 │ Flyway │ Liquibase │ │
│ ├────────────────┼──────────────────┼──────────────────┤ │
│ │ 첫 릴리스 │ 2010년 │ 2006년 │ │
│ │ 마이그레이션 │ SQL 파일 기반 │ XML/YAML/JSON/SQL│ │
│ │ 변경 정의 │ 직접 SQL 작성 │ 추상화된 changeset│ │
│ │ 학습 곡선 │ 낮음 (SQL만) │ 중간 (DSL 학습) │ │
│ │ DB 추상화 │ 없음 (DB별 SQL) │ 있음 (DB 독립적) │ │
│ │ 롤백 │ Enterprise만 │ 모든 에디션 │ │
│ │ 이력 테이블 │ flyway_schema_ │ databasechange │ │
│ │ │ history │ log │ │
│ │ Spring Boot │ 자동 설정 내장 │ 자동 설정 내장 │ │
│ │ 커뮤니티 │ 활발 │ 활발 │ │
│ │ 상용 지원 │ Redgate │ Liquibase Inc. │ │
│ │ 철학 │ SQL-first │ Abstraction-first│ │
│ └────────────────┴──────────────────┴──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6.2 각각의 마이그레이션 파일 비교
┌─────────────────────────────────────────────────────────────────┐
│ 같은 작업, 다른 방식 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 작업: users 테이블에 phone 컬럼 추가 │
│ │
│ Flyway (SQL 직접 작성): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ -- V4__Add_phone_to_users.sql │ │
│ │ ALTER TABLE users ADD COLUMN phone VARCHAR(20); │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Liquibase (XML changeset): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ <!-- changelog.xml --> │ │
│ │ <changeSet id="4" author="developer"> │ │
│ │ <addColumn tableName="users"> │ │
│ │ <column name="phone" type="VARCHAR(20)"/> │ │
│ │ </addColumn> │ │
│ │ </changeSet> │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Liquibase (YAML changeset): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ databaseChangeLog: │ │
│ │ - changeSet: │ │
│ │ id: 4 │ │
│ │ author: developer │ │
│ │ changes: │ │
│ │ - addColumn: │ │
│ │ tableName: users │ │
│ │ columns: │ │
│ │ - column: │ │
│ │ name: phone │ │
│ │ type: VARCHAR(20) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비교 분석: │
│ ├── Flyway: 1줄 SQL로 끝 → 단순, 직관적 │
│ ├── Liquibase XML: 5줄 → 타이핑 많지만 DB 독립적 │
│ ├── Liquibase YAML: 9줄 → 읽기 쉬우나 들여쓰기 실수 위험 │
│ └── 핵심: Flyway는 "SQL을 잘 아는 팀"에 최적화 │
│ │
└─────────────────────────────────────────────────────────────────┘
6.3 어떤 걸 선택해야 하는가?
┌─────────────────────────────────────────────────────────────────┐
│ 선택 가이드 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Flyway를 선택하라: │
│ ├── SQL에 익숙한 팀 │
│ ├── 단일 DB 벤더 (PostgreSQL만, MySQL만) │
│ ├── 단순함을 선호하는 팀 │
│ ├── Spring Boot + JPA/Hibernate 기반 프로젝트 │
│ └── "마이그레이션 도구 학습에 시간 쓰기 싫다" │
│ │
│ Liquibase를 선택하라: │
│ ├── 멀티 DB 지원 필요 (Oracle + PostgreSQL 동시) │
│ ├── 모든 에디션에서 롤백이 필요 │
│ ├── 복잡한 changelog 관리가 필요 │
│ ├── DB 변경을 SQL이 아닌 선언적으로 정의하고 싶다 │
│ └── 비개발자(DBA)도 변경 내역을 쉽게 읽어야 한다 │
│ │
│ 결론: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ JVM + 단일 DB + 단순함 선호 → Flyway │ │
│ │ 멀티 DB + 롤백 필수 + 선언적 접근 → Liquibase │ │
│ │ │ │
│ │ 사실 둘 다 훌륭한 도구. "아무거나 하나만 쓰세요"가 │ │
│ │ "도구 없이 수동 관리"보다 1000배 낫다. │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
7. 프로덕션 운영 베스트 프랙티스
7.1 마이그레이션 작성 원칙
┌─────────────────────────────────────────────────────────────────┐
│ 프로덕션 마이그레이션 작성 5대 원칙 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 원칙 1: 적용된 파일은 절대 수정하지 않는다 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 적용된 V 파일 수정 → checksum mismatch → 장애 │ │
│ │ 수정이 필요하면 → 새로운 V 파일로 변경 사항 작성 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 원칙 2: 스키마 변경과 데이터 변경을 분리한다 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Bad: V5에 ALTER TABLE + UPDATE 1000만 행 같이 작성 │ │
│ │ Good: V5 = ALTER TABLE (스키마), V6 = UPDATE (데이터) │ │
│ │ → 실패 시 어디서 실패했는지 명확 │ │
│ │ → 재실행 범위가 최소화됨 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 원칙 3: 마이그레이션 파일도 코드 리뷰한다 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SQL 파일을 Git에 커밋 → PR 리뷰 필수 │ │
│ │ 리뷰 체크리스트: │ │
│ │ ├── 인덱스 전략이 적절한가? │ │
│ │ ├── 대형 테이블 변경 시 락 전략이 있는가? │ │
│ │ ├── 롤백 계획이 있는가? │ │
│ │ └── 기존 데이터에 영향은 없는가? │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 원칙 4: 트랜잭션으로 감싸라 (지원되는 경우) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Flyway 기본 설정: 각 마이그레이션을 트랜잭션으로 실행 │ │
│ │ ├── PostgreSQL: DDL 트랜잭션 지원 → 안전 │ │
│ │ ├── MySQL: DDL 자동 커밋 → 트랜잭션 무의미 │ │
│ │ └── 대형 마이그레이션은 group=true로 묶기 가능 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 원칙 5: 프로덕션 데이터 복사본에서 테스트한다 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 프로덕션 DB 복사 → 마이그레이션 적용 → 검증 │ │
│ │ ├── 실행 시간 측정 (10분 넘으면 전략 변경 필요) │ │
│ │ ├── 락 발생 여부 확인 │ │
│ │ └── 애플리케이션 호환성 테스트 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
7.2 Zero-Downtime Migration: Expand-Contract 패턴
┌─────────────────────────────────────────────────────────────────┐
│ Expand-Contract (Parallel Change) 패턴 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 목표: 서비스 중단 없이 스키마 변경 │
│ │
│ 예시: users.name → users.first_name + users.last_name 분리 │
│ │
│ Step 1: EXPAND (확장) - 새 컬럼 추가 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ -- V10__Expand_add_name_columns.sql │ │
│ │ ALTER TABLE users ADD COLUMN first_name VARCHAR(100); │ │
│ │ ALTER TABLE users ADD COLUMN last_name VARCHAR(100); │ │
│ │ │ │
│ │ 테이블 상태: │ │
│ │ users: [id, name, email, first_name(null), last_name(null)]│ │
│ │ → 기존 앱은 name 컬럼만 사용 → 정상 동작 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Step 2: MIGRATE (데이터 이관) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ -- V11__Backfill_name_columns.sql │ │
│ │ UPDATE users SET │ │
│ │ first_name = SPLIT_PART(name, ' ', 1), │ │
│ │ last_name = SPLIT_PART(name, ' ', 2) │ │
│ │ WHERE first_name IS NULL; │ │
│ │ │ │
│ │ → 배치로 나눠서 실행 (한 번에 1만 건씩) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Step 3: SWITCH (앱 전환) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 앱 코드 변경: name → first_name + last_name 사용 │ │
│ │ 양쪽 컬럼 모두에 쓰는 과도기 코드 배포 │ │
│ │ → 새 앱은 first_name/last_name 읽기/쓰기 │ │
│ │ → 구 앱은 여전히 name 읽기/쓰기 (호환성 유지) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Step 4: CONTRACT (축소) - 이전 컬럼 제거 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ -- V12__Contract_remove_name_column.sql │ │
│ │ ALTER TABLE users DROP COLUMN name; │ │
│ │ │ │
│ │ → 모든 앱이 새 컬럼을 사용하는 것 확인 후 실행 │ │
│ │ → 최소 1~2주 대기 후 contract 실행 권장 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 전체 흐름: │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ EXPAND │→│MIGRATE │→│ SWITCH │→│CONTRACT│ │
│ │ 새 컬럼 │ │데이터 │ │앱 전환 │ │구 컬럼 │ │
│ │ 추가 │ │이관 │ │ │ │제거 │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
│ │
│ 핵심: 어느 단계에서든 롤백 가능하고, 서비스 중단 없음 │
│ │
└─────────────────────────────────────────────────────────────────┘
7.3 명명 규칙 전략
┌─────────────────────────────────────────────────────────────────┐
│ 팀을 위한 명명 규칙 가이드 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 전략 1: 순차 번호 (소규모 팀, 1~3명) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ V1__Create_users_table.sql │ │
│ │ V2__Add_email_to_users.sql │ │
│ │ V3__Create_orders_table.sql │ │
│ │ │ │
│ │ 장점: 단순, 직관적 │ │
│ │ 단점: 팀원 충돌 가능 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 전략 2: 타임스탬프 (중규모 팀, 4~15명) ← 권장! │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ V20260227143000__Create_users_table.sql │ │
│ │ V20260227150000__Add_email_to_users.sql │ │
│ │ V20260228090000__Create_orders_table.sql │ │
│ │ │ │
│ │ 장점: 충돌 거의 없음, 생성 시점 파악 가능 │ │
│ │ 단점: 파일명이 길어짐 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 전략 3: 하이브리드 (대규모 팀, 15명+) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ V20260227_001__Create_users_table.sql │ │
│ │ V20260227_002__Add_email_to_users.sql │ │
│ │ V20260228_001__Create_orders_table.sql │ │
│ │ │ │
│ │ 장점: 날짜별 그룹핑 + 순서 명확 │ │
│ │ 단점: 관리 규칙 합의 필요 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 설명(Description) 작성 규칙: │
│ ├── 동사로 시작: Create_, Add_, Drop_, Alter_, Fix_ │
│ ├── 대상 포함: _users_table, _to_orders, _index_on_email │
│ ├── 축약 금지: Crt_usr (X) → Create_users_table (O) │
│ └── 영어 사용: 한글은 인코딩 문제 위험 │
│ │
└─────────────────────────────────────────────────────────────────┘
8. Spring Boot 통합과 고급 패턴
8.1 Spring Boot 자동 설정
┌─────────────────────────────────────────────────────────────────┐
│ Spring Boot + Flyway 통합 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Spring Boot는 Flyway를 자동으로 구성한다! │
│ │
│ 의존성 추가만 하면 끝: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # build.gradle.kts │ │
│ │ dependencies { │ │
│ │ implementation("org.flywaydb:flyway-core") │ │
│ │ // MySQL 사용 시 │ │
│ │ implementation("org.flywaydb:flyway-mysql") │ │
│ │ } │ │
│ │ │ │
│ │ <!-- pom.xml --> │ │
│ │ <dependency> │ │
│ │ <groupId>org.flywaydb</groupId> │ │
│ │ <artifactId>flyway-core</artifactId> │ │
│ │ </dependency> │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 기본 동작: │
│ ├── 앱 시작 시 자동으로 flyway migrate 실행 │
│ ├── 마이그레이션 파일 위치: src/main/resources/db/migration/ │
│ ├── DataSource 빈을 자동으로 사용 │
│ └── Hibernate보다 먼저 실행됨 (FlywayMigrationInitializer) │
│ │
│ 실행 순서: │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ DataSource │→│ Flyway │→│ Hibernate │ │
│ │ 초기화 │ │ migrate │ │ validate │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
8.2 주요 설정 옵션
# application.yml - Flyway 상세 설정
spring:
flyway:
# 기본 설정
enabled: true # Flyway 활성화 (기본: true)
locations: classpath:db/migration # 마이그레이션 파일 경로
baseline-on-migrate: true # 기존 DB에 자동 baseline
baseline-version: 0 # baseline 버전
# 검증 설정
validate-on-migrate: true # migrate 전 validate 실행
validate-migration-naming: true # 파일명 규칙 검증
out-of-order: false # 순서 이탈 허용 여부
# 안전 설정
clean-disabled: true # clean 명령어 비활성화 (프로덕션!)
# Placeholder 설정
placeholder-replacement: true # placeholder 치환 활성화
placeholders:
schema_name: my_schema
table_prefix: app_
# 고급 설정
encoding: UTF-8 # 파일 인코딩
table: flyway_schema_history # 이력 테이블명 (커스터마이징 가능)
schemas: public # 관리할 스키마
default-schema: public # 기본 스키마
# Hibernate와 함께 사용할 때
jpa:
hibernate:
ddl-auto: none # 반드시 none! Flyway가 관리
8.3 환경별 설정 분리
┌─────────────────────────────────────────────────────────────────┐
│ 환경별 Flyway 설정 전략 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ application-local.yml (로컬 개발) │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ spring: │ │ │
│ │ │ flyway: │ │ │
│ │ │ clean-disabled: false ← 로컬에서만 clean 허용│ │ │
│ │ │ locations: │ │ │
│ │ │ - classpath:db/migration │ │ │
│ │ │ - classpath:db/testdata ← 테스트 데이터 │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ application-staging.yml (스테이징) │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ spring: │ │ │
│ │ │ flyway: │ │ │
│ │ │ clean-disabled: true │ │ │
│ │ │ validate-on-migrate: true │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ application-prod.yml (프로덕션) │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ spring: │ │ │
│ │ │ flyway: │ │ │
│ │ │ clean-disabled: true ← 절대 clean 불가 │ │ │
│ │ │ validate-on-migrate: true │ │ │
│ │ │ baseline-on-migrate: false ← 실수 방지 │ │ │
│ │ │ out-of-order: false ← 순서 강제 │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
8.4 Flyway Callback 활용
┌─────────────────────────────────────────────────────────────────┐
│ Flyway Callback = 마이그레이션 이벤트 후킹 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 사용 가능한 Callback 시점: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ beforeMigrate → 마이그레이션 시작 전 │ │
│ │ beforeEachMigrate→ 각 마이그레이션 파일 실행 전 │ │
│ │ afterEachMigrate → 각 마이그레이션 파일 실행 후 │ │
│ │ afterMigrate → 마이그레이션 완료 후 │ │
│ │ beforeValidate → 검증 시작 전 │ │
│ │ afterValidate → 검증 완료 후 │ │
│ │ beforeClean → clean 실행 전 │ │
│ │ afterClean → clean 실행 후 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 활용 예시 - SQL Callback: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 파일: db/migration/afterMigrate__refresh_views.sql │ │
│ │ │ │
│ │ -- 모든 마이그레이션 후 뷰 갱신 │ │
│ │ REFRESH MATERIALIZED VIEW CONCURRENTLY user_stats; │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 활용 예시 - Java Callback: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ @Component │ │
│ │ public class FlywayCallbackConfig │ │
│ │ implements Callback { │ │
│ │ │ │
│ │ @Override │ │
│ │ public boolean supports(Event event, Context ctx) { │ │
│ │ return event == Event.AFTER_MIGRATE; │ │
│ │ } │ │
│ │ │ │
│ │ @Override │ │
│ │ public void handle(Event event, Context ctx) { │ │
│ │ log.info("마이그레이션 완료! 캐시 초기화..."); │ │
│ │ cacheManager.clearAll(); │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
8.5 Kubernetes / Docker 환경
┌─────────────────────────────────────────────────────────────────┐
│ 컨테이너 환경에서의 Flyway 전략 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 전략 1: Init Container (권장) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # Kubernetes Deployment │ │
│ │ apiVersion: apps/v1 │ │
│ │ kind: Deployment │ │
│ │ spec: │ │
│ │ template: │ │
│ │ spec: │ │
│ │ initContainers: │ │
│ │ - name: flyway-migrate │ │
│ │ image: flyway/flyway:10 │ │
│ │ command: ["flyway", "migrate"] │ │
│ │ env: │ │
│ │ - name: FLYWAY_URL │ │
│ │ value: jdbc:postgresql://db:5432/myapp │ │
│ │ volumeMounts: │ │
│ │ - name: migrations │ │
│ │ mountPath: /flyway/sql │ │
│ │ containers: │ │
│ │ - name: app │ │
│ │ image: myapp:latest │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 장점: │
│ ├── 마이그레이션과 앱 실행이 분리됨 │
│ ├── 마이그레이션 실패 시 앱이 시작되지 않음 (안전) │
│ └── 여러 Pod 중 하나만 마이그레이션 실행 (경쟁 방지) │
│ │
│ 전략 2: 앱 내장 (Spring Boot 기본) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 앱 시작 시 자동 실행 (기본 동작) │ │
│ │ │ │
│ │ 주의: 여러 Pod 동시 시작 시 │ │
│ │ → Flyway가 DB 레벨 락으로 보호 (한 번만 실행) │ │
│ │ → 나머지 Pod는 마이그레이션 완료까지 대기 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 전략 3: 별도 Job (CI/CD에서) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # Kubernetes Job │ │
│ │ apiVersion: batch/v1 │ │
│ │ kind: Job │ │
│ │ metadata: │ │
│ │ name: flyway-migrate-v10 │ │
│ │ spec: │ │
│ │ template: │ │
│ │ spec: │ │
│ │ containers: │ │
│ │ - name: flyway │ │
│ │ image: flyway/flyway:10 │ │
│ │ command: ["flyway", "migrate"] │ │
│ │ restartPolicy: Never │ │
│ │ backoffLimit: 3 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
8.6 Multi-Tenancy 패턴
┌─────────────────────────────────────────────────────────────────┐
│ 멀티테넌시 + Flyway │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 패턴 1: Database-per-Tenant │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 각 테넌트가 별도의 데이터베이스를 가짐 │ │
│ │ │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │tenant_a│ │tenant_b│ │tenant_c│ │ │
│ │ │ DB │ │ DB │ │ DB │ │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ │ ↑ ↑ ↑ │ │
│ │ └────────────┼────────────┘ │ │
│ │ │ │ │
│ │ 동일한 마이그레이션을 각각 적용 │ │
│ │ │ │
│ │ 구현: 루프로 각 DB에 Flyway 실행 │ │
│ │ tenants.forEach(tenant -> { │ │
│ │ Flyway.configure() │ │
│ │ .dataSource(tenant.getUrl(), ...) │ │
│ │ .load() │ │
│ │ .migrate(); │ │
│ │ }); │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 패턴 2: Schema-per-Tenant │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 하나의 DB에서 테넌트별 스키마 분리 │ │
│ │ │ │
│ │ ┌─────────────── 하나의 DB ──────────────┐ │ │
│ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │
│ │ │ │schema_a │ │schema_b │ │schema_c │ │ │ │
│ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │
│ │ └────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 구현: schemas 설정으로 각 스키마에 적용 │ │
│ │ Flyway.configure() │ │
│ │ .schemas("tenant_a") │ │
│ │ .load() │ │
│ │ .migrate(); │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
9. HikariCP와 Flyway의 관계
9.1 Spring Boot 시작 순서 (Startup Order)
┌─────────────────────────────────────────────────────────────────┐
│ Spring Boot 애플리케이션 시작 순서 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ① HikariCP DataSource 초기화 │
│ ├── 커넥션 풀 생성 (minimum-idle 만큼 미리 연결) │
│ └── DB 연결 가능 여부 확인 │
│ │ │
│ ▼ │
│ ② FlywayMigrationInitializer → Flyway.migrate() 실행 │
│ ├── flyway_schema_history 확인 │
│ ├── 적용되지 않은 마이그레이션 스크립트 실행 (blocking) │
│ └── 완료 전까지 다음 단계로 진행하지 않음 │
│ │ │
│ ▼ │
│ ③ Hibernate/JPA EntityManagerFactory 생성 │
│ ├── ddl-auto: validate → 현재 스키마 검증 │
│ └── 엔티티 ↔ 테이블 매핑 확인 │
│ │ │
│ ▼ │
│ ④ @PostConstruct, ApplicationRunner, CommandLineRunner │
│ └── 비즈니스 초기화 로직 실행 │
│ │ │
│ ▼ │
│ ⑤ 애플리케이션 시작 완료 │
│ │
│ 핵심 보장 메커니즘: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ FlywayMigrationInitializerEntityManagerFactory │ │
│ │ DependsOnPostProcessor │ │
│ │ │ │
│ │ → Spring Boot 자동 설정이 이 클래스를 통해 │ │
│ │ EntityManagerFactory가 반드시 Flyway 완료 후 │ │
│ │ 생성되도록 의존 관계를 자동으로 주입 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ⚠ CRITICAL EDGE CASE: 커스텀 @Bean Flyway 등록 시 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ @Bean │ │
│ │ public Flyway flyway(DataSource ds) { // 이렇게 하면! │ │
│ │ return Flyway.configure().dataSource(ds).load(); │ │
│ │ } │ │
│ │ │ │
│ │ → DependsOnPostProcessor 등록이 비활성화됨 │ │
│ │ → Hibernate가 Flyway보다 먼저 실행될 수 있음 │ │
│ │ → 결과: 엔티티 검증 실패 또는 빈 DB에서 validate 오류 │ │
│ │ │ │
│ │ 해결책 1: @Bean(initMethod = "migrate") 사용 │ │
│ │ @Bean(initMethod = "migrate") │ │
│ │ public Flyway flyway(DataSource ds) { ... } │ │
│ │ │ │
│ │ 해결책 2: FlywayMigrationInitializer를 직접 주입 │ │
│ │ @Bean │ │
│ │ public FlywayMigrationInitializer flywayInitializer( │ │
│ │ Flyway flyway) { │ │
│ │ return new FlywayMigrationInitializer(flyway, null); │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
9.2 Flyway가 사용하는 커넥션 수
┌─────────────────────────────────────────────────────────────────┐
│ Flyway 버전별 사용 커넥션 수 (PostgreSQL 기준) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┬──────────┬───────────────────────────┐ │
│ │ Flyway 버전 │ 커넥션 수 │ 비고 │ │
│ ├─────────────────────┼──────────┼───────────────────────────┤ │
│ │ < 7.3.2 │ 1 │ 단일 커넥션으로 모든 작업 │ │
│ │ 9.1.0+ (PostgreSQL) │ 2 │ 트랜잭션 advisory lock │ │
│ │ 11.16.0 │ 3 │ 회귀 버그 (11.17.1에서 수정)│ │
│ └─────────────────────┴──────────┴───────────────────────────┘ │
│ │
│ Flyway 9.1.0 PostgreSQL 변경 사항: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 변경 전 (< 9.1.0): Session-level Advisory Lock │ │
│ │ ├── pg_try_advisory_lock(hash) → 커넥션 1개로 충분 │ │
│ │ └── 커넥션이 끊어지면 락 자동 해제 │ │
│ │ │ │
│ │ 변경 후 (>= 9.1.0): Transactional Advisory Lock │ │
│ │ ├── pg_try_advisory_xact_lock(hash) │ │
│ │ ├── 트랜잭션 범위 내에서만 유효 │ │
│ │ ├── 락 전용 커넥션(A) + 마이그레이션 커넥션(B) 필요 │ │
│ │ └── 최소 2개 커넥션 필요 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ maximum-pool-size가 너무 작으면 발생하는 문제: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 예: maximum-pool-size: 2, Flyway 9.1+, PostgreSQL │ │
│ │ │ │
│ │ Flyway가 커넥션 A, B를 모두 점유 │ │
│ │ → HikariCP 풀이 고갈됨 │ │
│ │ → Flyway 자신이 다음 커넥션을 기다리며 deadlock 유발 │ │
│ │ │ │
│ │ 에러 메시지: │ │
│ │ HikariPool-1 - Connection is not available, │ │
│ │ request timed out after 30000ms │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 권장 최소 maximum-pool-size: 5 │
│ ├── Flyway 9.1+ PostgreSQL: 2개 사용 │
│ └── 앱 시작 시 나머지 3개 여유분 확보 │
│ │
└─────────────────────────────────────────────────────────────────┘
9.3 Connection Timeout과 장시간 마이그레이션
┌─────────────────────────────────────────────────────────────────┐
│ Flyway 이중 커넥션 아키텍처와 장시간 마이그레이션 문제 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Flyway 이중 커넥션 구조: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 커넥션 A (메타데이터 락) │ │
│ │ ├── flyway_schema_history 테이블 락 보유 │ │
│ │ ├── 마이그레이션 전체 기간 동안 유지 │ │
│ │ └── SQL 실행 없이 유휴 상태 (idle) │ │
│ │ │ │ │
│ │ 커넥션 B (실제 실행) │ │ │
│ │ ├── 실제 마이그레이션 SQL 실행 │ │
│ │ └── ALTER TABLE, CREATE INDEX 등 │ │
│ │ │ │
│ │ 문제: 수백만 행 테이블의 ALTER TABLE 실행 시 │ │
│ │ → 커넥션 B: 수 분~수십 분 동안 SQL 실행 중 │ │
│ │ → 커넥션 A: 유휴 상태로 같은 시간 대기 │ │
│ │ → 방화벽/프록시가 유휴 커넥션을 강제 종료! │ │
│ │ → 커넥션 A 끊어짐 → 락 해제 → 마이그레이션 무결성 훼손 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 문제가 되는 중간 계층: │
│ ├── PgBouncer (connection pooler) │
│ ├── AWS NLB / ALB (idle timeout 기본 60초~350초) │
│ ├── 방화벽 (stateful inspection timeout) │
│ └── 클라우드 DB 프록시 (RDS Proxy, Cloud SQL Auth Proxy) │
│ │
│ 해결책: HikariCP keepalive-time 설정 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ spring: │ │
│ │ datasource: │ │
│ │ hikari: │ │
│ │ keepalive-time: 300000 # 5분마다 ping │ │
│ │ # 방화벽 idle timeout보다 짧게 설정 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 관련 HikariCP 속성 정리: │
│ ┌──────────────────────┬──────────────┬────────────────────┐ │
│ │ Property │ 기본값 │ 설명 │ │
│ ├──────────────────────┼──────────────┼────────────────────┤ │
│ │ connectionTimeout │ 30,000ms │ 풀 커넥션 대기 시간 │ │
│ │ maxLifetime │ 1,800,000ms │ 커넥션 최대 수명 │ │
│ │ │ (30분) │ │ │
│ │ keepaliveTime │ 0 (비활성) │ 유휴 커넥션 ping │ │
│ │ idleTimeout │ 600,000ms │ 유휴 커넥션 제거 │ │
│ │ │ (10분) │ │ │
│ └──────────────────────┴──────────────┴────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
9.4 전용 마이그레이션 커넥션 (spring.flyway.url)
┌─────────────────────────────────────────────────────────────────┐
│ Flyway 전용 DataSource 분리 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 기본 동작 (spring.flyway.url 미설정): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ HikariCP Pool ←──── Flyway (풀에서 커넥션 빌림) │ │
│ │ ←──── 애플리케이션 (동일 풀 사용) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ spring.flyway.url/user/password 설정 시: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ HikariCP Pool ←──── 애플리케이션 (앱 전용) │ │
│ │ Plain JDBC ←──── Flyway (별도 커넥션, HikariCP 아님) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 활용 사례 1: 최소 권한 원칙 (Least Privilege) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 마이그레이션 유저: CREATE, ALTER, DROP, REFERENCES │ │
│ │ 애플리케이션 유저: SELECT, INSERT, UPDATE, DELETE만 │ │
│ │ │ │
│ │ # application.yml │ │
│ │ spring: │ │
│ │ datasource: │ │
│ │ url: jdbc:postgresql://db:5432/myapp │ │
│ │ username: app_user # 앱 전용 (제한된 권한) │ │
│ │ password: ${APP_PASSWORD} │ │
│ │ flyway: │ │
│ │ url: jdbc:postgresql://db:5432/myapp │ │
│ │ user: migration_user # 마이그레이션 전용 │ │
│ │ password: ${MIGRATION_PASSWORD} │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 활용 사례 2: @FlywayDataSource 어노테이션 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ @Bean │ │
│ │ @FlywayDataSource │ │
│ │ public DataSource flywayDataSource() { │ │
│ │ return DataSourceBuilder.create() │ │
│ │ .url("jdbc:postgresql://db:5432/myapp") │ │
│ │ .username("migration_user") │ │
│ │ .password(migrationPassword) │ │
│ │ .build(); │ │
│ │ } │ │
│ │ // @Primary DataSource는 앱 유저로 HikariCP 사용 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ⚠ 알려진 버그: spring.flyway.init-sqls 무시 문제 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Spring Boot < ~2.5 에서: │ │
│ │ spring.flyway.init-sqls 설정이 spring.flyway.url이 │ │
│ │ 함께 설정되지 않으면 조용히(silently) 무시됨 │ │
│ │ │ │
│ │ 참고: Spring Boot Issue #23392 │ │
│ │ 해결: spring.flyway.url도 함께 설정하거나 │ │
│ │ Spring Boot 버전 업그레이드 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
9.5 프로덕션 통합 설정 (Production Configuration)
# application.yml - HikariCP + Flyway 프로덕션 통합 설정
spring:
datasource:
url: jdbc:postgresql://db:5432/myapp
username: ${DB_APP_USER}
password: ${DB_APP_PASSWORD}
hikari:
maximum-pool-size: 10 # Flyway 2개 + 앱 8개 여유
minimum-idle: 5 # 최소 유지 커넥션
keepalive-time: 300000 # 5분마다 유휴 커넥션 ping
connection-timeout: 30000 # 30초 대기 후 실패
max-lifetime: 1800000 # 30분 커넥션 최대 수명
idle-timeout: 600000 # 10분 유휴 후 제거
flyway:
url: jdbc:postgresql://db:5432/myapp
user: ${DB_MIGRATION_USER} # 마이그레이션 전용 유저
password: ${DB_MIGRATION_PASSWORD}
clean-disabled: true # 프로덕션 clean 절대 금지
validate-on-migrate: true # 마이그레이션 전 무결성 검증
connect-retries: 5 # DB 연결 재시도 횟수
connect-retries-interval: 10 # 재시도 간격 (초), Docker 환경 필수
jpa:
hibernate:
ddl-auto: validate # Flyway가 관리, Hibernate는 검증만
┌─────────────────────────────────────────────────────────────────┐
│ 프로덕션 배포 체크리스트 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 마이그레이션 유저와 앱 유저 분리 │
│ └── 최소 권한 원칙: 앱 유저는 DML만, 마이그레이션 유저는 DDL │
│ │
│ 2. maximum-pool-size >= 5 (Flyway 9.1+ PostgreSQL) │
│ └── Flyway 2개 + 앱 기동 시 필요한 여유분 확보 │
│ │
│ 3. clean-disabled: true 설정 확인 │
│ └── 프로덕션 DB 전체 삭제 사고 방지 │
│ │
│ 4. ddl-auto: validate 설정 │
│ └── Hibernate가 스키마를 직접 변경하지 않도록 차단 │
│ │
│ 5. keepalive-time 설정 (장시간 마이그레이션 시) │
│ └── 유휴 커넥션 A가 방화벽에 의해 끊기지 않도록 유지 │
│ │
│ 6. connect-retries + connect-retries-interval 설정 │
│ └── 컨테이너 환경에서 DB 기동 전 앱이 먼저 뜰 때 대비 │
│ │
│ 7. 프로덕션 데이터 볼륨으로 마이그레이션 사전 테스트 │
│ └── 개발 DB (소량)와 프로덕션 DB (수백만 행)의 실행 시간 차이 │
│ │
└─────────────────────────────────────────────────────────────────┘
9.6 알려진 버그와 주의사항
┌─────────────────────────────────────────────────────────────────┐
│ Flyway + HikariCP 알려진 버그 및 주의사항 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Bug 1: Flyway 11.16.0 - 3 커넥션 회귀 버그 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 증상: maximum-pool-size: 2 이하 환경에서 기동 hang │ │
│ │ 원인: 커넥션을 3개 획득하도록 회귀 │ │
│ │ 수정: Flyway 11.17.1 업그레이드 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Bug 2: Flyway 9.1.0 PostgreSQL - 트랜잭션 락 2 커넥션 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 증상: Connection is not available, timed out after Nms │ │
│ │ 원인: session-level → transactional advisory lock 변경 │ │
│ │ 수정: maximum-pool-size >= 5 설정 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Bug 3: Spring Boot < 2.5 - init-sqls 조용히 무시 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 증상: spring.flyway.init-sqls 설정이 적용 안 됨 │ │
│ │ 원인: spring.flyway.url 없이는 init-sqls 무시 (Issue │ │
│ │ #23392) │ │
│ │ 수정: spring.flyway.url 함께 설정 또는 버전 업그레이드 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Bug 4: 커스텀 @Bean Flyway - DependsOnPostProcessor 비활성 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 증상: Hibernate가 Flyway보다 먼저 실행되어 validate 실패 │ │
│ │ 원인: 자동 설정 우회로 의존 순서 보장 메커니즘 해제 │ │
│ │ 수정: FlywayMigrationInitializer Bean 수동 등록 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Bug 5: Flyway 7.15+ Docker - connect-retries 재시도 너무 빠름 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 증상: Docker Compose 환경에서 DB 기동 전 재시도 모두 소진│ │
│ │ 원인: connect-retries 기본 재시도 간격이 너무 짧음 │ │
│ │ 수정: connect-retries-interval: 10 (초) 이상 설정 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Bug 6: 장시간 마이그레이션 - 유휴 메타데이터 커넥션 끊김 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 증상: 수십 분 마이그레이션 도중 커넥션 오류 발생 │ │
│ │ 원인: 방화벽/NLB가 유휴 커넥션 A를 idle timeout으로 종료 │ │
│ │ 수정: hikari.keepalive-time을 방화벽 timeout보다 짧게 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
10. 모던 대안과 비교
10.1 DB 마이그레이션 도구 생태계
┌─────────────────────────────────────────────────────────────────┐
│ 2026년 DB 마이그레이션 도구 비교 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┬────────────┬─────────────┬────────────┐ │
│ │ 도구 │ 생태계 │ 접근 방식 │ 특징 │ │
│ ├──────────────┼────────────┼─────────────┼────────────┤ │
│ │ Flyway │ JVM │ SQL-first │ 단순, 강력 │ │
│ │ Liquibase │ JVM │ Changelog │ DB 독립적 │ │
│ │ Atlas │ Go/범용 │ Declarative │ 자동 diff │ │
│ │ Alembic │ Python │ Migration │ SQLAlchemy │ │
│ │ Prisma Migrate│ Node.js │ Declarative │ TypeScript │ │
│ │ golang-migrate│ Go │ SQL-first │ 경량 │ │
│ │ sqitch │ 범용 │ Change mgmt │ ORM 독립 │ │
│ │ Knex.js │ Node.js │ Code-first │ 쿼리빌더 │ │
│ │ Django │ Python │ Code-first │ 내장 │ │
│ └──────────────┴────────────┴─────────────┴────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
10.2 주요 대안 상세 비교
┌─────────────────────────────────────────────────────────────────┐
│ 대안 도구 상세 분석 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Atlas (by Ariga) - 선언적 스키마 관리 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 특징: │ │
│ │ ├── "원하는 상태"를 선언하면 자동으로 마이그레이션 생성 │ │
│ │ ├── 현재 DB 상태와 원하는 상태를 자동 diff │ │
│ │ ├── HCL(HashiCorp Configuration Language) 사용 │ │
│ │ └── Terraform처럼 DB 스키마를 "선언적"으로 관리 │ │
│ │ │ │
│ │ 예시: │ │
│ │ schema "public" { │ │
│ │ table "users" { │ │
│ │ column "id" { type = int } │ │
│ │ column "email" { type = varchar(255) } │ │
│ │ primary_key { columns = [column.id] } │ │
│ │ } │ │
│ │ } │ │
│ │ → atlas schema apply --auto-approve │ │
│ │ → 자동으로 ALTER TABLE 생성! │ │
│ │ │ │
│ │ Flyway와 비교: │ │
│ │ ├── Flyway: 개발자가 직접 ALTER SQL 작성 (명시적) │ │
│ │ └── Atlas: 도구가 자동으로 ALTER SQL 생성 (편리) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Alembic (Python / SQLAlchemy) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ├── Python 생태계의 표준 마이그레이션 도구 │ │
│ │ ├── SQLAlchemy 모델에서 자동으로 마이그레이션 감지 │ │
│ │ ├── alembic revision --autogenerate │ │
│ │ └── FastAPI, Flask 프로젝트에서 필수 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Prisma Migrate (Node.js / TypeScript) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ├── Prisma Schema에서 자동으로 마이그레이션 생성 │ │
│ │ ├── TypeScript 타입 자동 생성 (DX 최고) │ │
│ │ ├── prisma migrate dev → 개발, prisma migrate deploy │ │
│ │ └── Next.js, NestJS 프로젝트에서 인기 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ golang-migrate │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ├── Go 생태계의 경량 마이그레이션 도구 │ │
│ │ ├── Flyway와 유사하게 SQL 파일 기반 │ │
│ │ ├── CLI + Go 라이브러리 모두 지원 │ │
│ │ └── Docker, Kubernetes 친화적 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Flyway를 고수해야 할 때: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ├── JVM 생태계 (Spring Boot, Kotlin, Java) │ │
│ │ ├── SQL-first 철학 공감 │ │
│ │ ├── 엔터프라이즈 지원이 필요 (Redgate) │ │
│ │ ├── 이미 Flyway를 사용 중 (전환 비용 > 이점) │ │
│ │ └── 팀이 SQL에 능숙하고 자동 생성을 불신 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11. 소프트웨어 엔지니어를 위한 교훈
11.1 핵심 원칙
┌─────────────────────────────────────────────────────────────────┐
│ DB 마이그레이션에서 배우는 엔지니어링 원칙 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 원칙 1: "Database changes should be as reviewable │
│ as code changes" │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SQL 파일 = 코드 파일 │ │
│ │ ├── Git으로 버전 관리 │ │
│ │ ├── PR에서 코드 리뷰 │ │
│ │ ├── CI에서 자동 테스트 │ │
│ │ └── 프로덕션 배포 파이프라인에 통합 │ │
│ │ │ │
│ │ 더 이상 "DBA가 프로덕션에서 직접 ALTER TABLE" 하는 │ │
│ │ 시대는 끝났다. │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 원칙 2: Evolutionary Database Design │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Martin Fowler의 핵심 주장: │ │
│ │ │ │
│ │ "완벽한 스키마를 미리 설계하려 하지 마라. │ │
│ │ 요구사항이 진화하듯, 스키마도 진화한다. │ │
│ │ 중요한 것은 안전하게 진화할 수 있는 인프라다." │ │
│ │ │ │
│ │ ├── Big Design Up Front (X) │ │
│ │ ├── Iterative Schema Evolution (O) │ │
│ │ ├── 작은 변경을 자주, 안전하게 │ │
│ │ └── Flyway = 안전한 진화를 가능하게 하는 인프라 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 원칙 3: Schema as Code │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Infrastructure as Code (IaC)의 DB 버전: │ │
│ │ │ │
│ │ IaC : Terraform → 인프라 상태를 코드로 관리 │ │
│ │ Schema as : Flyway → DB 스키마 상태를 코드로 관리 │ │
│ │ Code │ │
│ │ │ │
│ │ 공통 원칙: │ │
│ │ ├── 선언적 또는 절차적으로 상태 정의 │ │
│ │ ├── 버전 관리 시스템에 저장 │ │
│ │ ├── 자동화된 적용 (수동 개입 최소화) │ │
│ │ └── 감사 가능한 변경 이력 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 원칙 4: Forward-Only Migration │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 롤백 문화 vs 전진 문화: │ │
│ │ │ │
│ │ 전통적: 문제 발생 → 이전 버전으로 롤백 │ │
│ │ 현대적: 문제 발생 → 수정 버전을 새로 배포 (전진) │ │
│ │ │ │
│ │ Flyway Community Edition이 undo를 제공하지 않는 이유: │ │
│ │ ├── 완벽한 롤백은 대부분 불가능 (데이터 손실) │ │
│ │ ├── 롤백에 의존하면 마이그레이션 품질이 떨어짐 │ │
│ │ └── "고장난 것을 고치는 게 되돌리는 것보다 안전하다" │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 원칙 5: 마이그레이션 파일은 불변 프로덕션 아티팩트이다 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Docker Image처럼: │ │
│ │ ├── 한번 빌드되면 수정하지 않는다 │ │
│ │ ├── 태그(버전)로 식별한다 │ │
│ │ └── 문제 시 새 버전을 만든다 │ │
│ │ │ │
│ │ Flyway Migration 파일도: │ │
│ │ ├── 한번 적용되면 수정하지 않는다 │ │
│ │ ├── 버전 번호로 식별한다 │ │
│ │ └── 문제 시 새 마이그레이션 파일을 만든다 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.2 최종 요약
┌─────────────────────────────────────────────────────────────────┐
│ Flyway 한눈에 정리 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Flyway란? │
│ → SQL 기반의 데이터베이스 스키마 버전 관리 도구 │
│ │
│ 핵심 가치: │
│ ├── 단순함: SQL만 알면 된다 │
│ ├── 신뢰성: checksum으로 무결성 보장 │
│ ├── 추적성: flyway_schema_history로 완전한 감사 이력 │
│ └── 자동화: CI/CD와 완벽한 통합 │
│ │
│ 핵심 명령어: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ migrate → 마이그레이션 적용 │ │
│ │ info → 상태 확인 │ │
│ │ validate → 무결성 검증 │ │
│ │ baseline → 기존 DB에 Flyway 도입 │ │
│ │ repair → 문제 복구 │ │
│ │ clean → 전체 삭제 (개발 전용!) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 베스트 프랙티스: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 적용된 파일 수정 금지 │ │
│ │ 타임스탬프 기반 버전 번호 사용 │ │
│ │ 스키마 변경과 데이터 변경 분리 │ │
│ │ Hibernate ddl-auto=none 설정 │ │
│ │ Expand-Contract 패턴으로 Zero-Downtime 달성 │ │
│ │ 프로덕션 데이터 복사본에서 사전 테스트 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 한 줄 요약: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ "데이터베이스 변경을 코드처럼 관리하라. │ │
│ │ Flyway는 그것을 가장 단순하게 실현하는 도구다." │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
관련 키워드
Flyway, Database Migration, 스키마 마이그레이션, flyway_schema_history, Versioned Migration, Repeatable Migration, Undo Migration, migrate, clean, info, validate, baseline, repair, checksum, Schema Drift, Expand-Contract Pattern, Zero-Downtime Migration, Liquibase, Atlas, Alembic, Prisma Migrate, Spring Boot, Hibernate ddl-auto, CI/CD, Kubernetes Init Container, Multi-Tenancy, Flyway Callback, Forward-Only Migration, Schema as Code, Evolutionary Database Design, Martin Fowler, Axel Fontaine, Redgate, SQL-first, HikariCP, Connection Pool, maximum-pool-size, keepaliveTime, connectionTimeout, FlywayMigrationInitializer, DependsOnPostProcessor, @FlywayDataSource, init-sqls, connect-retries