website: 마이그레이션 버전 번호 충돌 CI 가드 + 타임스탬프 버전 컨벤션
서로 무관한 두 PR이 같은 버전 번호로 Flyway 마이그레이션 파일을 만들면서 벌어진 테스트 무더기 실패를 CI 가드와 버전 컨벤션 변경으로 막은 작업이다.
시작은 CI 실패였다. MemberUniqueConstraintUpgradeTest, PostAuthorUpgradeTest, AiMemberRoleUpgradeTest 같은, 이번에 손댄 적도 없는 마이그레이션 테스트들이 한꺼번에 실패하고 있었다. 딱 봐서는 테스트 코드나 SQL 자체에 문제가 있어 보였는데, 실제로 확인해보니 원인은 그쪽이 아니었다. 지원폼 마이그레이션과 모집상태 마이그레이션, 서로 관계 없는 두 PR이 각자 V7__... 파일을 만들었고, 그 사이 dev에는 V7__add_staff_activities.sql이 먼저 머지돼 있었다. 두 PR 다 dev와 합쳐지는 순간 Flyway가 CompositeMigrationResolver에서 버전 중복을 감지하고 FlywayException을 던졌고, 이게 Flyway를 쓰는 모든 테스트의 Spring 컨텍스트 기동 자체를 막아버린 것이었다. 각 PR을 단독으로 보면 아무 문제가 없었다는 게 이 상황을 더 헷갈리게 만든 부분이었다 — 문제는 코드가 아니라 "둘 다 dev를 보고 다음 번호를 추측한다"는 컨벤션 자체의 레이스였다.
원인을 파악하고 나니 손댈 지점이 두 개로 나뉘었다. 하나는 애초에 이런 충돌이 일어날 확률을 줄이는 것, 다른 하나는 그래도 겹쳤을 때 머지 전에 잡아내는 것.
첫 번째는 버전 넘버링 방식을 바꾸는 걸로 해결했다. 기존엔 ls로 현재 가장 큰 V{n}을 확인하고 그 다음 번호를 붙이는 방식이었는데, 이 방식 자체가 "지금 dev에 뭐가 있는지" 봐야 다음 번호를 알 수 있다는 전제를 깔고 있다. 동시에 개발되는 두 브랜치가 같은 시점에 같은 걸 보고 있으면 필연적으로 같은 번호를 잡게 된다. 그래서 순번 대신 타임스탬프(V<yyyyMMddHHmmss>__설명.sql)를 쓰기로 했다. Flyway는 버전을 숫자로만 비교하기 때문에 기존 V1~V7을 리네임할 필요 없이, 타임스탬프 버전이 자연히 그 뒤에 붙는다. 이렇게 하면 각자 로컬 시계만 보면 되니 "다음 번호가 몇인지" 조율하는 과정 자체가 사라진다.
다만 타임스탬프로 바꾼다고 충돌 가능성이 완전히 0이 되는 건 아니다(같은 초에 커밋하는 극단적인 경우 등). 그래서 두 번째로 migration-guard CI 잡에 검사 스텝을 하나 추가했다.
declare -A seen
DUPES=()
for f in backend/src/main/resources/db/migration/V*.sql; do
base=f")
version=base" | sed -E 's/^V([0-9]+(\.[0-9]+)*)__.*/\1/')
if [ -n "{seen[version]}" ]; then
DUPES+=("버전 {seen[version]} ↔base")
else
seen[base"
fi
done
이 스텝을 짜면서 신경 쓴 부분은 actions/checkout이 pull_request 이벤트에서 기본으로 체크아웃하는 게 PR merge ref라는 점이었다. 즉 이 시점에 디렉터리엔 머지 대상 브랜치와 이 PR 양쪽 마이그레이션 파일이 이미 다 들어와 있다는 뜻이라, 별도로 git diff를 떠서 비교할 필요 없이 지금 있는 파일들만 스캔해서 버전 번호(V 다음, __ 앞) 중복을 잡으면 됐다. 실제로 이 로직이 제대로 동작하는지도 확인해야 했는데, V7__add_staff_activities.sql과 그걸 복제해 버전만 겹치게 만든 파일을 로컬에 놓고 스크립트를 돌려서 정확히 중복을 잡아내는 걸 본 다음 그 테스트용 파일을 지웠다.
문서 쪽은 이 판단을 남기는 데 썼다. backend/.claude/skills/db-man/SKILL.md의 마이그레이션 작성 절차에서 "다음 버전 번호 확인 후" 항목을 "버전은 타임스탬프로"로 바꾸고, 왜 순번이 아니라 타임스탬프인지 이번 사고 경위를 그대로 적어뒀다. backend/CLAUDE.md에도 같은 내용을 간단히 반영했고, pm/docs/learnings.md에는 이번 사고의 원인과 재발 방지 컨벤션을 한 줄로 남겼다. 이런 종류의 문제는 코드 변경보다 다음에 같은 실수를 반복하지 않게 기록해두는 쪽이 실질적으로 더 중요하다고 생각해서, 변경 규모(추가 42줄, 삭제 5줄)에 비해 문서 비중을 크게 뒀다.
커밋은 실질적으로 하나였고("ci(migration): 마이그레이션 버전 번호 충돌 CI 가드 + 타임스탬프 버전 컨벤션 도입") dev를 한 번 머지해 받아온 뒤 바로 병합됐다. 이번 건에서 남는 생각은, 마이그레이션 파일명처럼 "각자 보고 다음 값을 추측하는" 방식은 동시성이 생기는 순간 전부 레이스가 될 수 있다는 점이다. 순번 기반 넘버링을 쓰는 다른 곳이 있다면 비슷한 방식으로 점검해볼 필요가 있어 보인다.