website: stage DB 스냅샷 대안, 전 스키마가 뭘 허용했었길래 이론상 아직 남아 있을 수 있는가
어제 남긴 V6 마이그레이션 인시던트 학습 기록에 대안을 하나 적어뒀는데, 다시 들여다보니 그 대안 자체가 틀렸다는 걸 확인해서 정정했다.
사건은 이랬다. MemberRole을 6개(PM/FE/BE/DESIGN/AI/INFRA)에서 14개로 확장하면서 새 CHECK 제약으로 테이블을 재생성하고 insert ... select member_id, role from member_roles로 기존 값을 그대로 복사하는 마이그레이션을 짰다. 그런데 stage DB에 남아 있던 BE/FE/PM/INFRA 중 PM·INFRA는 새 목록에 아예 없는 값이라 제약을 위반했고, Flyway가 즉시 실패 → 앱 기동 불가 → 헬스체크 타임아웃 → 자동 롤백으로 이어졌다. 당일 hotfix(#253)로 CASE를 써서 BE→BACKEND, FE→FRONTEND로 매핑하고 PM·INFRA는 행을 삭제해 수습했다.
원인을 파고들어보니 마이그레이션 SQL 문제가 아니었다. AiMemberRoleUpgradeTest라는 업그레이드 테스트가 이미 존재했는데, 신구 CHECK의 교집합인 'DESIGN' 딱 하나만 심어놓고 "호환되는 값이 살아남는지"만 검증하고 있었다. 실제 stage에 쌓여 있던 BE/FE/PM/INFRA는 하나도 시드하지 않은 채였다. CI가 도는 임시 SQLite는 매번 빈 파일에서 시작하니 이 괴리가 로컬/CI에서는 전혀 드러나지 않았다.
여기까지 정리하면서 처음엔 "실제 stage DB 스냅샷 위에서 마이그레이션을 검증하는 CI 단계를 추가하자"를 대안으로 적었다. 데이터 기반 검증이니 사람이 뭘 기억하든 자동으로 걸러질 거라고 봤다.
근데 다시 생각해보니 이 대안은 두 지점에서 무너졌다. 하나는 stage의 성격을 잘못 이해한 것이었다 — stage는 "테스트 환경"이라 데이터가 익명화돼 있을 거라고 막연히 가정했는데, 실제로는 데이터 격리(운영 트래픽과 섞이지 않게)가 목적일 뿐이고 안에는 14기 조직의 진짜 멤버 이름·역할·학번이 그대로 들어 있었다(members.student_id, 공개 동의 로직까지 존재). 이 스냅샷을 CI로 끌어오는 것 자체가 PII 노출 표면을 넓히는 일이었다. 다른 하나는 ci.yml 구조를 놓친 것이었다 — dev PR과 main PR에 완전히 동일한 잡이 돈다. stage 스냅샷으로 검증해봐야 그건 stage 데이터 기준의 검증일 뿐, dev→main PR이 실제로 위협하는 건 prod 데이터인데 그건 전혀 커버가 안 된다. prod까지 안전하게 하려면 별도로 prod 스냅샷을 CI에 또 들여야 하는데, 그건 stage보다 훨씬 민감해서 더 어려운 문제였다.
그래서 방향을 바꿨다. 필요한 건 "지금 DB에 실제로 뭐가 있는가"가 아니라 "예전 스키마가 뭘 허용했었길래 이론상 아직 남아 있을 수 있는가"였다. 이 정보는 실 DB가 아니라 과거 마이그레이션 SQL 파일 자체에서 나온다 — V1부터 V(n-1)까지 그 컬럼에 대해 CHECK로 허용했던 값의 합집합을 시드로 쓰는 정적 방식이다. PII 접근이 전혀 필요 없고, stage든 prod든 똑같이 적용된다.
실행 메커니즘은 MigrationUpgradeHarness로 옛 버전(예: V5)까지 마이그레이션한 뒤, 그 시점 CHECK가 허용하던 값 전부를 한 행씩 심고, 대상 마이그레이션(V6)까지 마저 적용해서 예외 없이 끝나는지, 그리고 각 값이 의도한 대로(매핑됐는지 혹은 의도적으로 삭제됐는지) 처리됐는지를 어서션하는 식이다. 이번 사고의 진짜 결함은 "V5까지 유효했던 값 6개 중 1개만 시드했다"는 누락이었으니, 시드 목록 자체를 사람이 기억해서 타이핑하지 않고 이전 마이그레이션 파일의 CHECK 목록에서 기계적으로 도출하거나, 최소한 "V1..V5 CHECK 값 합집합"이라는 주석과 함께 한곳에 모아두고 PR 리뷰에서 그 목록 자체를 대조하도록 해야 같은 누락이 재발하지 않는다.
커밋 자체는 pm/docs/learnings.md 한 파일에 두 줄만 추가된 작은 문서 수정이지만, 하루 전에 내린 판단을 다음 날 스스로 다시 검증해서 뒤집은 기록이라 남겨둔다. 대안을 적어두고 끝내는 게 아니라, "이게 진짜 맞나"를 한 번 더 따져보는 과정 자체가 이 학습 문서의 목적에 더 맞는다고 생각한다.