website: email_log.email_type CHECK 제약에 빠진 RECRUITMENT_OPEN 추가
모집 시작 안내 메일을 보내면 메일 자체는 나가는데, 발송 기록을 남기는 email_log 저장이 CHECK 제약 위반으로 실패하는 문제를 고쳤다.
증상을 확인해보니 email_log 테이블의 email_type 컬럼에 걸린 CHECK 제약이 INVITE, PASSWORD_RESET 두 값만 허용하고 있었다. 그런데 코드 쪽 EmailType.java는 이미 한참 전부터 RECRUITMENT_OPEN을 정상적인 값으로 다루고 있었다. 코드와 실제 DB 제약이 서로 어긋나 있다는 걸 알고 있는 상태였다.
이 어긋남이 어디서 왔는지 추적해보니 세 단계가 겹쳐 있었다. 처음 모집 관리 기능(#124)을 만들면서 EmailType enum에 RECRUITMENT_OPEN을 추가했는데, 그때 서버는 ddl-auto: update를 쓰고 있었다. 이 방식은 Java 코드가 바뀌면 DB 스키마도 알아서 맞춰주는데, SQLite는 이미 있는 컬럼의 CHECK 제약 내용을 ALTER TABLE로 바꾸는 걸 지원하지 않는다. 문제는 ddl-auto: update가 이 실패를 에러 없이 조용히 삼켰다는 점이다. 그래서 코드에는 3번째 값이 추가됐는데 실제 DB의 제약은 여전히 2개 값에 머물러 있는 상태가 됐고, 아무도 눈치채지 못했다.
그 뒤 Flyway를 도입(#133)하면서 "지금 시점 코드가 기대하는 전체 스키마"를 V1__baseline.sql로 캡처했다. 이 시점 코드는 이미 RECRUITMENT_OPEN을 알고 있었으니 파일에는 3개 값이 올바르게 적혔다. 다만 stage/prod는 Flyway 도입 이전부터 운영되던 DB라 이 파일을 그대로 실행하면 "테이블이 이미 있다"는 에러가 난다. 그래서 baseline-on-migrate로 "이 DB는 이미 V1과 같은 상태다"라고 선언하고 넘어가는 방식을 썼는데, 이건 실제로 대조하는 게 아니라 같다고 믿고 넘어가는 방식이다. 그 믿음이 실제로는 틀렸던 거고, 이걸 걸러줄 것 같은 ddl-auto: validate(같은 #133에서 도입)도 컬럼 존재 여부나 타입만 검사할 뿐 CHECK 제약 안의 값 목록까지는 보지 않아서 이 문제를 못 걸렀다. 그 결과 앱은 정상적으로 뜨고, 실제로 RECRUITMENT_OPEN 값으로 저장을 시도하는 순간에야 DB가 거부하는 상황이 만들어졌다.
고치는 방법 자체는 CHECK 제약에 값 하나를 추가하는 단순한 작업이지만, SQLite가 CHECK 제약을 ALTER TABLE로 바로 못 고치는 건 이전과 동일하다. 그래서 이번에도 새 테이블을 만들고, 기존 데이터를 옮기고, 옛 테이블을 지우고, 이름을 바꾸는 재생성 패턴을 그대로 썼다.
create table email_log_new (
id integer,
email_type varchar(255) not null check (email_type in ('INVITE', 'PASSWORD_RESET', 'RECRUITMENT_OPEN')),
...
);
insert into email_log_new (...)
select ... from email_log;
drop table email_log;
alter table email_log_new rename to email_log;
기존 값(INVITE, PASSWORD_RESET)은 그대로 두고 값을 하나 추가하는 것뿐이라 데이터 손실은 없다.
검증은 두 갈래로 했다. EmailLogEmailTypeCheckWideningUpgradeTest는 마이그레이션 적용 전 상태(V20260730133717까지)를 재현해서 기존 행 두 개를 심어두고, 마이그레이션 후에도 그 행들이 남아있는지, 그리고 RECRUITMENT_OPEN 값으로 새로 저장하는 게 이제 성공하는지 확인한다. SchemaMigrationConsistencyIntegrationTest는 전체 마이그레이션을 처음부터 순서대로 적용한 빈 DB에서 앱이 정상 기동하는지, 즉 엔티티와 스키마가 어긋나지 않는지를 본다. 두 테스트 모두 로컬에서 통과를 확인했다. Mailpit을 띄우는 Docker 기반 통합 테스트 7개는 이번 변경과 무관하고 검증 환경에 Docker가 없어서 돌리지 못했다.
이 건은 결국 "ddl-auto: update의 조용한 실패 → 그 실패가 반영 안 된 상태를 baseline이 '맞다'고 전제 → 그 전제를 검증해야 할 validate가 CHECK 제약 값 목록까지는 안 본다"는 세 가지가 순서대로 겹쳐서 아무도 모르게 방치된 케이스였다. 마이그레이션 파일 주석에 이 경위를 그대로 남겨뒀는데, enum 값을 추가/변경할 때 validate나 로컬 테스트로는 CHECK 제약 드리프트를 못 잡는다는 걸 다음에 같은 실수를 반복하지 않기 위한 참고로 적어둔 것이다. PR은 dev 브랜치로 올라간 지 9분 만에 병합됐고, stage 배포 후 실제 RECRUITMENT_OPEN 메일 발송으로 재확인하는 작업이 남아 있다.