74 lines
7.5 KiB
Markdown
74 lines
7.5 KiB
Markdown
---
|
||
title: error / flyway-launcher-divergent-migration-set-shared-dev-db-2026-06-12
|
||
source_type: error-note
|
||
status: raw
|
||
related_branches: [feature-domain-event-outbox-contract, feature-persistence-auditing-contract]
|
||
related_projects: [ca-skeleton]
|
||
tags: [error, ca-skeleton, flyway, migration, classpath, gradle, ide, testcontainers]
|
||
created: 2026-06-12
|
||
status_label: resolved
|
||
---
|
||
|
||
# error: flyway-launcher-divergent-migration-set-shared-dev-db-2026-06-12
|
||
|
||
> Layer: `raw/errors/` — 작업 중 마주친 단일 실패·트러블슈팅 기록. 원본은 raw에 영구 보관한다.
|
||
|
||
## Parent / 부모
|
||
|
||
- [[raw/branch-notes/feature-domain-event-outbox-contract]] — V3__outbox_event.sql 추가가 잠복해 있던 V2 위치 결함을 발화시킴. V2 자체는 feature-persistence-auditing-contract 산출물.
|
||
|
||
## 증상 / Symptom
|
||
|
||
- 에러 메시지 (사용자 IDE 실행, 원문 그대로):
|
||
```text
|
||
Error creating bean with name 'flywayInitializer' ... : Flyway forward-only migration failed during startup
|
||
```
|
||
- 스크래치 DB 재현 시 실제 원인 메시지: `Validate failed: Migrations have failed validation` (Flyway 11.7.2).
|
||
- 발생 컨텍스트: 같은 로컬 dev PostgreSQL(`ca-pg`, localhost:5432/ca_skeleton)을 Gradle bootRun 과 IDE Run 이 공유. **Gradle bootRun 은 정상 기동, IDE Run 만 실패** — 동일 코드, 동일 DB.
|
||
- 재현 가능 여부: `always` (클래스패스 조합 재현 시).
|
||
|
||
## 재현 절차 / Reproduction
|
||
|
||
전제: `V1__idempotency_record.sql`(adapter-persistence), `V2__work_log.sql`(sample-portfolio, 기본 `db/migration`), `V3__outbox_event.sql`(adapter-persistence). app-bootstrap 은 sample-portfolio 를 `testImplementation` 으로만 의존.
|
||
|
||
1. Gradle bootRun (runtime classpath — V2 미포함) → Flyway 가 {V1,V3} 해석·적용. history = {1,3}.
|
||
2. IDE Run (VSCode/JDT — test 의존성이 클래스패스에 합류해 V2 가 보임) → 해석 {V1,V2,V3}, history {1,3} → V2 가 max(3) 아래 미적용 → **resolved-not-applied 검증 실패** (out-of-order=false 는 FLYWAY-C5 로 고정).
|
||
3. 반대 방향도 확인: outOfOrder=true 로 V2 를 보정 적용해 history={1,2,3} 을 만들면, 이번엔 Gradle 실행(해석 {V1,V3})이 **applied-not-resolved 검증 실패**. 즉 어느 쪽으로 "고쳐도" 다른 launcher 가 깨짐.
|
||
4. 위 1–3 은 Flyway 11.7.2 단독 하네스(java single-file + filesystem locations + 스크래치 DB)로 4-시나리오 전부 실측 (STEP1 OK / STEP2 FAIL / STEP3 OK / STEP4 FAIL).
|
||
|
||
## 조사 단계 / Investigation log
|
||
|
||
- 2026-06-12 — 사용자가 "여전히 Flyway 오류" 보고. `ca-pg` 는 Up, 5432 리스닝, Gradle bootRun 은 2회 연속 정상 기동 → connection refused 아님, launcher 차이로 압축.
|
||
- 2026-06-12 — `flyway_schema_history` = {1, 3}, 레포 마이그레이션 = V1/V2/V3. V2 는 sample-portfolio 소속 + app-bootstrap `testImplementation` 전용 → Gradle 런타임에서 V2 비가시 확인.
|
||
- 2026-06-12 — Gradle cache 의 flyway-core 11.7.2 + flyway-database-postgresql + pg driver + jackson 으로 단독 하네스 구성, 스크래치 DB 에서 4-시나리오 실측 → 양방향 검증 실패 확정.
|
||
- 2026-06-12 — V2 소비자 전수 조사: 샘플 테스트는 실 DB/Flyway 미사용(mock), `OutboxContainerTestSupport` 는 `classpath:db/migration` 마이그레이션이지만 outbox/idempotency 테이블만 사용, application.yml locations 미지정(기본값), compose init 마운트 없음 → V2 이동의 파급 없음 확인.
|
||
|
||
## 근본 원인 / Root cause
|
||
|
||
- 직접 원인: V3 적용(2026-06-12 Gradle 실행) 시점에 V2 가 런타임 클래스패스에 없어 history 에 V2 구멍이 생김 → V2 가 보이는 launcher 의 검증 실패.
|
||
- 근본 원인: **fixture 모듈(sample-portfolio)의 마이그레이션이 production 과 같은 기본 location(`db/migration`)·같은 버전 네임스페이스를 공유하면서, launcher 별로 클래스패스 합류 여부가 달라짐** — 하나의 long-lived dev DB 에 대해 "해석되는 마이그레이션 집합"이 실행 방법에 따라 달라지는 구조. V2 파일 자체의 주석("production 은 V1 만 돈다")은 V3 등장 전의 가정.
|
||
- 트리거 조건: 기본 location 의 fixture 마이그레이션 + 그보다 큰 버전의 production 마이그레이션 추가 + launcher 간 클래스패스 차이 + 공유 dev DB.
|
||
|
||
## Sources / 근거
|
||
|
||
- 로컬 검증: Flyway 11.7.2 단독 하네스 4-시나리오 실측 출력 (STEP1 OK migrationsExecuted=2 / STEP2 FAIL Validate failed / STEP3 OK migrationsExecuted=1 / STEP4 FAIL Validate failed) — `locally-verified`.
|
||
- ca-tmpl `application.yml` L133-135: `out-of-order: false` 주석 "reject out-of-order migrations — preserve cross-developer ordering consistency (FLYWAY-C5). Enabling under prod is forbidden (D4)." — 보정 적용(outOfOrder) 경로가 계약상 막혀 있음의 근거.
|
||
- Flyway 의 location 재귀 스캔/검증 규칙에 대한 공식 문서 인용은 미보강 (`needs-confirmation` — flywaydb.org locations/validate 절 인용 권고).
|
||
|
||
## 해결 / Resolution
|
||
|
||
- 적용한 조치: `V2__work_log.sql` 을 `sample-portfolio/src/main/resources/db/migration/` → `db/sample-migration/` (기본 스캔 위치 밖 sibling) 으로 `git mv`. 파일 헤더의 낡은 가정 문단을 "왜 이 위치인가 + 활성화 방법(`spring.flyway.locations` 에 location 추가) + 로컬은 ddl-auto=update 가 sample 스키마 담당" 으로 교체. 결과: 모든 launcher 가 동일하게 {V1,V3} 해석 → 현 dev DB history {1,3} 과 일치 → 양쪽 검증 통과. DB 데이터/이력 무변경 (work_log 테이블은 기존 ddl-auto 산출물 그대로).
|
||
- 검증 방법: bootRun 기동 3.324s + healthcheck 200 + ERROR 0건; `:sample-portfolio:test` 129/129, `:app-bootstrap:test` 224/224 (Testcontainers outbox 계약 5종 — V1+V3 만 적용으로도 green, ArchUnit 48 rules).
|
||
- 잔여 위험: IDE(JDT)가 이전 빌드 산출물(`build/resources/main/db/migration/V2__work_log.sql` 또는 JDT bin 출력)을 캐시하고 있으면 한 번 더 실패할 수 있음 — Java 프로젝트 reload/clean 필요. fork 한 프로젝트가 sample 을 런타임에 켜려면 location 추가가 필요함을 헤더에 명시.
|
||
|
||
## 회고 / Lessons
|
||
|
||
- 빨리 감지하는 신호: "Gradle 로는 되는데 IDE 로만 Flyway validate 실패" → launcher 별 클래스패스의 `db/migration` 자원 차이부터 비교 (`find */src/main/resources -path '*db/migration*'` + `flyway_schema_history` 대조).
|
||
- 예방 체크리스트: fixture/optional 모듈의 마이그레이션은 기본 `db/migration` 에 두지 않는다 (Flyway 는 location 을 클래스패스 루트 전체에서 재귀 스캔). 새 production 마이그레이션 버전을 딸 때 비-런타임 모듈에 더 낮은 미적용 버전이 남아 있는지 확인.
|
||
- 디버깅 기법: Flyway 동작이 기억과 다를 수 있는 검증 규칙(resolved-not-applied vs applied-not-resolved 의 fatal 여부)은 Gradle cache jar 로 1-파일 하네스를 만들어 스크래치 DB 에 실측하는 것이 추측보다 빠르다 (이번 4-시나리오 실측이 해결 방향을 결정).
|
||
- wiki 일반화 후보: "마이그레이션 집합은 클래스패스의 함수다 — launcher 가 둘이면 마이그레이션 소스도 둘" (wiki/concepts 추출 후보).
|
||
|
||
## Related / 관련
|
||
|
||
- 관련 에러: [[raw/errors/method-security-class-pointcut-final-usecase-bean-2026-06-12]] — 같은 날 같은 branch 의 직전 기동 실패 (bean 등록↔클래스 레벨 pointcut). 두 건 모두 "모듈 경계(테스트 전용 의존/샘플 fixture)가 런타임 배선과 만나는 지점"에서 터진 결함.
|