website: Flyway 10 cleanOnValidationError 제거 대응
Flyway 도입 직후 dev→stage CD가 실패해서, 원인을 추적해보니 Flyway 10에서 아예 사라진 프로퍼티 하나 때문이었고 이를 stage 전용 코드로 재구현했다.
#163에서 Flyway를 도입하고 머지한 직후 dev→stage CD가 실패했다. 다행히 자동 롤백이 걸려서 stage는 이전 이미지로 정상 복구됐고 데이터에 영향은 없었지만, 왜 실패했는지는 확인해야 했다.
로그를 보니 FlywayConfiguration 빈 생성 단계에서부터 막혀 있었다. .env.stage에 설정해둔 SPRING_FLYWAY_CLEAN_ON_VALIDATION_ERROR=true 때문이었는데, 이상한 건 이 값이 딱히 잘못된 값도 아니라는 점이었다. 찾아보니 Spring Boot 3.5.16이 끌어오는 Flyway 10.x에서 spring.flyway.clean-on-validation-error 프로퍼티 자체가 제거돼 있었다. 값을 뭘로 설정하든 상관없이, 이 프로퍼티가 존재하기만 해도 Spring이 없는 메서드를 호출하려다 예외를 던지는 구조였다.
왜 이걸 없앴는지 찾아보니 이유가 명확했다. Redgate(Flyway 유지보수사)가 "체크섬이 안 맞으면 DB를 통째로 지운다"는 동작을 위험한 기본값으로 판단해서 없앤 거였다. 실수로 이 옵션이 켜진 채 운영 DB가 날아간 사례들이 실제로 있었다고 한다. 설계 의도 자체는 합리적이라, 이걸 억지로 되살리기보다 같은 동작이 필요한 곳(stage)에서만 명시적으로 다시 구현하는 쪽으로 방향을 잡았다.
옵션은 두 가지였다. Flyway를 9버전대로 내려서 우회하거나, 같은 동작을 코드로 재구현하거나. 버전을 내리면 당장은 편하지만 나중에 또 같은 문제를 미루는 셈이라 재구현 쪽을 택했다. 대신 여기서 중요한 건 이 clean 동작이 prod에는 절대 새어 들어가면 안 된다는 점이었다. 그래서 @Profile("stage")로 한정한 FlywayMigrationStrategy 빈을 만들어서, 체크섬 불일치(FlywayValidateException)가 나면 stage에서만 clean 후 재적용하도록 했다.
@Bean
@Profile("stage")
public FlywayMigrationStrategy stageCleanOnValidationErrorStrategy() {
return flyway -> {
try {
flyway.migrate();
} catch (FlywayValidateException e) {
log.warn("stage 마이그레이션 체크섬 불일치 — clean 후 재적용합니다(stage 전용 정책)", e);
flyway.clean();
flyway.migrate();
}
};
}
prod는 이 빈 자체가 생성되지 않으니, 검증 실패 시 기존 Spring Boot 기본 동작(기동 실패 → 헬스체크 실패 → CD 자동 롤백)을 그대로 따르게 된다. env var 방식이었으면 .env.prod에 같은 변수를 실수로 복붙하는 순간 prod도 자동 clean 대상이 될 수 있었는데, @Profile 기반 분리는 그 실수 자체가 구조적으로 불가능하다는 점이 이 방식을 선택한 이유다.
빈 생성 여부를 프로파일별로 고정해두는 게 이후에도 이 안전장치가 깨지지 않는다는 걸 보장하는 핵심이라, FlywayConfigTest에서 ApplicationContextRunner로 stage/prod/무프로파일 세 경우를 각각 검증했다. Docker 없이 로컬에서 격리 실행되는 테스트라 CI 의존 없이 바로 확인할 수 있었다.
다만 이 커밋만으로 문제가 끝나는 건 아니었다. 크래시는 커스텀 빈이 실행되기 전, Flyway 인스턴스 생성 단계에서 나기 때문에 서버의 .env.stage에 남아있는 SPRING_FLYWAY_CLEAN_ON_VALIDATION_ERROR=true를 별도로 제거해야 코드 변경이 실제로 반영된다. .env.prod는 확인 결과 이 변수 자체가 없어서 안전했다. 로컬에서는 Docker가 필요한 testcontainers/SMTP 목 관련 7개 테스트를 제외한 전체가 통과했고, 이 7개는 로컬 환경 문제일 뿐 이번 변경과는 무관해서 CI에서는 정상 통과할 걸로 봤다. PR은 같은 날 바로 병합됐고, 남은 건 서버 쪽 env var 정리와 dev CD가 이번엔 헬스체크까지 통과하는지 확인하는 일이었다.