← all posts
DEV 2026.05.02 · 12 min read Intermediate

Flyway는 어떻게 마이그레이션을 신뢰하는가

flyway_schema_history의 체크섬 원리부터 동시성 Lock, 마이그레이션 유형 선택, Callback 자동화, 체크섬 불일치 해결까지 — Flyway 설계 철학의 다섯 층을 추적한다.


Flyway를 쓴다는 건 마이그레이션 파일 하나를 두 곳에 동시에 “기억”시킨다는 뜻이다. 하나는 파일 시스템이고, 다른 하나는 flyway_schema_history 테이블이다. 이 두 기억이 어긋나는 순간 Flyway는 앱 시작을 거부한다. 왜 이런 설계를 선택했고, 어디서 어긋날 수 있는가?

flyway_schema_history — 마이그레이션의 기억

Flyway의 모든 신뢰는 flyway_schema_history 테이블에서 출발한다. 이 테이블에는 10개 컬럼이 있는데, 실질적으로 중요한 것은 세 가지다: version, checksum, success.

Spring Boot가 시작하면 FlywayAutoConfiguration이 flyway.migrate()를 호출한다. 이 순간 Flyway는 다음 순서를 밟는다.

classpath:db/migration 스캔
    ↓
각 파일의 CRC32 체크섬 계산
    ↓
flyway_schema_history와 비교
    ↓
미적용 마이그레이션만 실행

테이블이 없으면 자동으로 생성한다. 있으면 기록된 체크섬과 현재 파일의 체크섬을 비교한다. 일치하면 스킵, 불일치하면 즉시 오류를 던진다. flyway_schema_history는 읽기 전용 원장에 가깝다 — 한 번 기록된 마이그레이션은 Flyway의 정상 경로로는 수정되지 않는다.

⚠ 흔한 실수

DELETE FROM flyway_schema_history를 실행하면 Flyway는 모든 마이그레이션이 미적용 상태라고 판단한다. 이미 존재하는 테이블을 다시 만들려고 시도하면서 앱 시작이 실패한다.

CRC32 — 파일의 지문

체크섬은 java.util.zip.CRC32로 계산한 32비트 정수다. 파일 내용이 1비트라도 바뀌면 값이 달라진다. Java의 int는 부호 있는 정수이므로 결과가 음수로 나올 수 있는데, 이는 정상이다.

CRC32 crc = new CRC32();
crc.update(content.getBytes(StandardCharsets.UTF_8));
int checksum = (int) crc.getValue(); // 음수 가능

문제는 “내용”의 정의가 생각보다 넓다는 점이다. 줄바꿈 방식(LF vs CRLF), BOM(Byte Order Mark), 공백과 탭 — 전부 체크섬에 영향을 준다. Windows 개발자와 Linux CI/CD 서버가 같은 파일을 다른 방식으로 저장하면, 코드는 동일해 보여도 체크섬이 다르다.

LF 버전:   ...0A 44 45 46...   → 체크섬: 111111
CRLF 버전: ...0D 0A 44 45 46... → 체크섬: 222222

이 문제의 근본 해결책은 .gitattributes로 줄바꿈을 통일하는 것이다.

*.sql text eol=lf

한 줄로 팀 전체의 CRLF 문제를 막는다.

Lock — 여러 Pod이 동시에 시작할 때

마이크로서비스 환경에서 Kubernetes Pod 10개가 동시에 뜨면 어떻게 되는가? Flyway는 flyway_schema_history_lock 테이블을 자동으로 생성하고, SELECT ... FOR UPDATE로 상호 배제를 구현한다.

Pod1: Lock 획득 → V1 실행 → Commit → Lock 해제
Pod2: Lock 대기 중...
Pod3: Lock 대기 중...
Pod2: Lock 획득 → V1 이미 적용됨 확인 → 스킵
Pod3: Lock 획득 → V1 이미 적용됨 확인 → 스킵

기본 재시도 횟수는 50회다. lock-retry-count를 너무 낮게 설정하면 GC pause처럼 일시적인 지연에도 Lock 획득에 실패해 앱 시작이 거부된다.

V, R, U — 마이그레이션의 세 가지 성격

Versioned(V), Repeatable(R), Undo(U)는 서로 다른 질문에 답한다.

V 마이그레이션은 “이 변경은 한 번만 일어난다”는 선언이다. 테이블 생성, 컬럼 추가, 인덱스 생성이 여기 속한다. 한 번 적용되면 파일 수정이 불가능하다 — 수정하면 체크섬이 달라지고 앱 시작이 거부된다.

R 마이그레이션은 “이 내용은 항상 최신 상태여야 한다”는 선언이다. 뷰, 함수, 권한 부여가 여기 속한다. 파일을 수정하면 체크섬이 달라지고, 다음 migrate 실행 시 자동으로 재실행된다. 단, 멱등성이 필수다 — CREATE OR REPLACE VIEW처럼.

-- R__create_views.sql
-- CREATE VIEW가 아니라 CREATE OR REPLACE VIEW
CREATE OR REPLACE VIEW user_summary AS
SELECT COUNT(*) as total FROM users;

실행 순서는 항상 V1 → V2 → V3 → R__a → R__b다. R은 모든 V가 끝난 뒤에 실행된다.

**Undo(U)**는 Flyway Teams 전용이다. 커뮤니티 버전을 쓴다면 Undo 개념 자체를 포기하고 “정방향만 진행”하는 편이 낫다. 실수한 마이그레이션은 새로운 V 마이그레이션으로 역처리한다.

✎ 트레이드오프

V 마이그레이션은 순서와 불변성을 보장하는 대신 수정 불가라는 제약을 수반한다. R 마이그레이션은 유연하지만 멱등성을 개발자가 직접 보장해야 한다. 뷰와 권한은 R, 스키마 변경은 V — 이 구분이 무너지면 예측 불가능한 실행이 발생한다.

Callback과 체크섬 불일치 — 자동화와 방어

afterMigrate.sql이라는 파일을 db/migration/에 두면, 모든 마이그레이션이 끝난 뒤 자동으로 실행된다. 권한 부여, 통계 갱신, Materialized View 새로고침을 여기 배치하면 수동 작업을 없앨 수 있다.

Java Callback(FlywayCallback 구현체)은 Slack 알림이나 캐시 초기화처럼 외부 시스템 연동이 필요할 때 쓴다. 단, Callback은 Spring Context 초기화 전에 실행되므로 @Autowired로 Bean을 주입받을 수 없다. 생성자 주입만 가능하다.

체크섬 불일치가 발생했을 때 flyway repair를 무작정 실행하는 건 위험하다. 올바른 순서는 다음과 같다.

원인 파악 (git diff)
    ↓
파일 복구 (git checkout)
    ↓
flyway validate 확인
    ↓
필요하면 flyway migrate

repair는 파일 내용과 DB 스키마가 실제로 일치한다고 확인한 뒤에만 쓴다. 모르는 상태에서 repair를 실행하면 flyway_schema_history의 체크섬만 갱신되고 실제 스키마 불일치는 남는다.

정리

  • flyway_schema_history는 마이그레이션의 원장이다. 수동으로 건드리면 다음 실행이 망가진다.
  • CRC32 체크섬은 파일의 지문이다. CRLF, BOM, 공백 변경도 감지한다. .gitattributes로 줄바꿈을 통일하라.
  • Lock은 DB만으로 분산 동시성 문제를 해결한다. lock-retry-count는 기본값(50)을 유지하라.
  • V는 불변, R은 재실행 가능 — 이 원칙이 무너지면 예측 불가능한 마이그레이션이 발생한다.
  • flyway repair는 원인 파악 없이 실행하지 마라. 항상 git diff로 확인 먼저.

다음 글에서는 DDL이 DB 내부에서 Lock을 거는 원리와, 무중단 마이그레이션을 위해 어떤 순서로 DDL을 작성해야 하는지 추적한다.