새 빌드를 올렸더니 기동하자마자 종료됐다
테이블에 컬럼 하나를 추가하는 변경이었다. 기존 데이터를 새 구조로 옮기는 마이그레이션도 함께 넣었고, 검증도 통과했다. 그리고 운용 폴더의 실행 파일을 교체하고 켰다.
프로그램이 즉시 죽었다. 자동매매가 10분간 멈췄다.
인덱스가 자기보다 늦게 생기는 컬럼을 참조하고 있었다
원인은 한 줄이었다. 새 컬럼을 빠르게 조회하려고 인덱스를 하나 만들었는데, 그 인덱스를 테이블 정의 쪽에 적어둔 것이다.
프로그램이 데이터베이스를 여는 순서는 이렇다.
1. 테이블 정의를 실행한다 (없으면 만들고, 있으면 넘어간다)
2. 마이그레이션을 실행한다 (구조가 낡았으면 새 구조로 고친다)
기존 데이터베이스에는 그 컬럼이 아직 없다. 컬럼을 만드는 것은 2번이다. 그런데 인덱스는 1번에 적혀 있었으니, 컬럼이 생기기 전에 그 컬럼을 참조하는 인덱스가 먼저 실행된다. 데이터베이스는 없는 컬럼을 색인할 수 없다고 거부했고, 여는 데 실패하면 프로그램은 더 진행할 수 없으므로 그대로 종료됐다.
마이그레이션 코드 자체는 멀쩡했다. 거기까지 도달하지 못한 것이다.
그 시점에 단위 테스트는 전량 통과였다
이 대목이 이 글을 쓰는 이유다. 결함이 운용을 멈춘 그 순간, 테스트는 하나도 빠짐없이 초록이었다.
테스트가 부실해서가 아니다. 테스트는 매번 새 빈 데이터베이스로 돌기 때문이다.
빈 데이터베이스에서는 1번이 테이블을 새로 만든다. 새로 만드는 정의에는 새 컬럼이 처음부터 들어 있으므로, 인덱스가 참조하는 컬럼은 이미 존재한다. 마이그레이션은 할 일이 없어서 조용히 넘어간다.
즉 이 결함은 기존 데이터가 있어야만 발현한다. 빈 상태에서는 원리상 재현이 불가능하다. 테스트를 더 짜도, 더 촘촘히 짜도 잡히지 않는다. 잡을 수 있는 조건 자체가 그 안에 없다.
기존 데이터베이스 사본으로 한 번만 띄워봤다면 즉시 드러났을 일이다.
그래서 처방도 테스트가 아니었다
고칠 것은 두 가지였다. 인덱스를 마이그레이션 뒤로 옮기는 것이 첫째고, 이건 쉬웠다.
둘째가 진짜다. 배포 앞에 운영 데이터 사본으로 새 빌드를 격리 기동해보는 절차를 세웠다. 실제 데이터를 복사해두고, 새 실행 파일이 그것을 정상적으로 여는지 확인한 다음에야 운용 폴더를 교체한다.
여기서 한 번 더 걸렸다. 그 절차를 만들어두고 "이제 됐다"고 적을 뻔했는데, 그 검사가 실제로 실패를 잡는지 확인하지 않은 상태였다. 통과했다는 신호와 아예 돌지 않았다는 신호가 구분되지 않으면 그것은 검사가 아니라 장식이다.
그래서 일부러 손상된 데이터베이스를 넣어봤다. 검사가 그것을 실패로 판정하는 것을 보고서야 장치가 됐다고 적었다.
남은 것
세 문장으로 정리된다.
빈 상태로만 도는 검증은 기존 상태에서 나는 결함을 잡을 수 없다. 전량 통과가 안전을 뜻하지 않는 경우가 있고, 이 경우가 정확히 그렇다.
순서가 있는 두 단계 사이에 의존이 생기면 그 의존은 코드에 안 보인다. 인덱스 한 줄은 어디에 적혀도 문법상 옳다. 틀린 것은 위치였고, 위치는 실행 순서를 알아야만 판단할 수 있다.
검사를 만들면 그 검사가 실패를 잡는지도 확인해야 한다. 확인하지 않은 검사는 통과와 미실행이 같은 모습이다.