도입과 철수 사이 22일
2026년 5월 13일 b832f20d로 Flyway를 처음 도입했고, 6월 4일 9e3a7aaa로 걷어냈다. 그 사이 22일 동안 FlywayConfig는 221줄까지 자라났다가 통째로 삭제됐다. 성공담이 아니라 철수 회고다.
왜 도입했나
이 프로젝트는 Gradle 멀티모듈 7개(common / shared / platform / feature_a / feature_b / integration / app)로 나뉘어 있고, 모듈 간 의존은 단방향이다. JPA ddl-auto만으로는 컬럼 타입 변경이나 제약 교체를 반영할 수 없어서, 스키마 변경을 코드로 관리할 도구가 필요했다. Flyway는 자연스러운 선택으로 보였다.
문제는 전제였다. Flyway는 빈 DB에서 시작해 마이그레이션으로 쌓아 올린 스키마를 가정한다. 이 프로젝트는 이미 81개 테이블이 존재하는 레거시 스키마를 가지고 있었다.
무엇이 계속 터졌나
1. 베이스라인 만들기부터 쉽지 않았다
8c27ab14에서 기존 스키마 덤프를 추출해 V1__initial_schema.sql을 만들었다. 그냥 덤프를 넣으면 되는 게 아니었다 — 멱등성을 위해 CREATE TABLE IF NOT EXISTS로 바꾸고, 뷰로 교체된 3개 테이블을 제외하고, 후속 마이그레이션과 충돌하는 컬럼을 빼고, 반대로 참조되는 컬럼은 더해야 했다. baselineVersion도 “1”에서 “0”으로 조정해야 fresh DB에서 V1이 baseline에 가려 스킵되지 않았다.
2. 모듈별로 쪼개자 실행 순서가 깨졌다
0e905c7e에서 마이그레이션을 platform / feature_a / feature_b 모듈로 분리했다. Spring Boot의 자동 Flyway를 끄고 모듈별 인스턴스를 수동 등록해 독립 history 테이블을 각각 뒀다.
그런데 모듈별로 순차 실행하면 날짜 순서와 실행 순서가 어긋난다. feature_a의 V20260515가 platform의 V20260522보다 날짜상 먼저인데, “platform 전체 → feature_a 전체” 순으로 돌리면 나중에 실행된다. 결과는 MySQL 1054 (Unknown column).
9814fccc에서 세 모듈의 pending 마이그레이션을 글로벌 버전순으로 정렬해 하나씩 적용하는 인터리브 로직을 FlywayConfig에 직접 구현했다. 프레임워크가 해주지 않는 일을 설정 클래스가 떠안기 시작한 지점이다.
3. collation 불일치
같은 커밋에서 collation 문제도 터졌다. 한 마이그레이션이 테이블을 utf8mb4_unicode_ci로 변환한 결과, 다른 마이그레이션의 UPDATE ... JOIN 비교에서 1267 (Illegal mix of collations)이 발생했다. 다수(381개 컬럼)에 맞춰 소수(13개)를 변환하는 마이그레이션을 out-of-order 슬롯에 끼워 넣어 수습했다.
4. FK 이름이 환경마다 달랐다
Hibernate가 자동 생성하는 FK 이름(FKr19hq... 형태)은 환경마다 다르다. 하드코딩한 DROP FOREIGN KEY가 fresh DB에서만 실패했다. information_schema로 FK 존재를 확인한 뒤 동적 실행하도록 바꿨고, 이게 이후 팀 규칙이 됐다.
결정적이었던 것
이 문제들의 공통점은 기존 DB에서는 안 터지고 fresh DB에서만 터진다는 것이었다. 점진적으로 적용된 로컬 DB에서는 멀쩡히 돌던 마이그레이션 세트가, 처음부터 전부 돌리면 부팅이 실패했다. 버전 충돌 수정 커밋만 5건 이상 쌓였다.
즉 Flyway를 쓰고 있는데도 “마이그레이션을 처음부터 돌리면 스키마가 재현된다”는 보장이 없었다. Flyway의 핵심 가치가 무너진 것이다. 그 상태를 유지하는 비용은 계속 들고 있었고.
걷어낸 뒤
9e3a7aaa에서 FlywayConfig 221줄을 삭제하고, Gradle 의존성과 yml 설정을 주석 처리했다. 마이그레이션 SQL 파일들은 이력·참고용으로 남겼다. 대신 dev DB dump import 방식으로 회귀했다 — 로컬 환경은 dev의 덤프를 받아 구성한다.
| 항목 | Flyway | dump import |
|---|---|---|
| 로컬 스키마 구성 | 마이그레이션 전량 재생 | dev 덤프 import |
| 실행 순서 관리 | 모듈 인터리브 직접 구현 | 불필요 |
| fresh DB 재현성 | 보장 실패 | dev와 동일 보장 |
| 스키마 변경 이력 | 파일로 추적 | 추적 약화 |
이력 추적이 약해진 건 명백한 손실이다. 이건 이후 별도의 보완 DDL 자동화로 다시 다루게 된다.
남는 교훈
Flyway가 나쁜 도구여서가 아니다. “멀티모듈 + 이미 존재하는 레거시 스키마”라는 조합에서 비용이 특히 비쌌다. 모듈이 여럿이면 마이그레이션 경로도 여럿이 되고, 그 순간 Flyway가 기본 제공하는 단일 버전 축이 깨진다. 순서 보장을 직접 구현하기 시작하면 이미 도구를 벗어난 것이다.
되짚어보면 신호는 일찍부터 있었다. FlywayConfig에 커스텀 로직이 붙기 시작한 시점, 그러니까 모듈별 분리(0e905c7e) 때 한 번 멈춰서 “이 도구가 이 구조에 맞는가”를 물었어야 했다고 본다. 대신 문제가 생길 때마다 설정 클래스를 키우는 쪽으로 3주를 갔다.