= [Dev] Testing [Dev] 스펙은 MySQL 이 아니라 MySQL 모드의 H2 에서 돈다. 스펙이 도는 스키마는 실제 데이터베이스의 커밋된 덤프 `schema/schema.sql` 에서 만든다. == 공용 하네스 `test/com/aha00a/tests` 아래 두 파일이 모든 스펙에 필요한 것을 들고 있다. * `TestApplication` - 인메모리 `SyncCacheApi` 대역(진짜 캐시 모듈은 Redis 인데 테스트는 그것을 띄우지 않는다), H2 URL, 충돌 없는 데이터베이스 이름, 그리고 모든 스펙이 출발하는 Guice 구성. * `TestSchema` - `createAll()` 이 덤프에서 모든 테이블을 만든다. 골라 만든 부분집합이 아니다: 외래 키가 테이블들을 묶고 있고, 스펙이 자기가 필요하다고 믿는 것을 나열하는 것이 그 목록이 낡는 방식이다. 인메모리 DB 에서 안 쓰는 테이블은 비용이 없다. 다른 것이 필요한 스펙은 공용 구성을 다시 적는 대신 거기에 얹는 것으로 말한다. `ApiV1FilterSpec` 은 진짜 필터 체인을 돌리므로 `play.http.filters` 만 덮고 나머지는 그대로 둔다. Guice 애플리케이션이 쓰는 데이터베이스 URL 과 직접 여는 `DriverManager` 연결의 URL 은 같은 `TestApplication.h2Url` 에서 와야 한다. 같은 이름의 철자 둘은 서로 다른 인메모리 데이터베이스 둘이고, 스펙은 불일치가 아니라 없는 테이블로 실패한다. == 스키마를 참으로 유지하기 스키마를 바꾸면 `schemaDump.sh` 로 덤프를 새로 뜬다. 스펙은 자동으로 따라온다. 스펙이 의존하는 컬럼이 사라지면 조용히 낡는 사본 대신 실패하는 스펙으로 나타난다. 스크립트는 `mysqldump` 가 쓰는 헤더를 지우는데, 지우는 이유가 둘이고 하나로 오해하기 쉽다. 호스트와 데이터베이스 이름은 공개 저장소에 있으면 안 되는 인프라 이름이라 로컬 설정을 가리키는 자리로 바꾼다. `mysqldump` 클라이언트 버전(`Distrib …, for …`)·완료 시각·`AUTO_INCREMENT` 카운터는 실행마다 바뀌는 잡음일 뿐인데, 두면 안 바뀐 스키마가 diff 로 도착한다. 서버 버전 줄(`-- Server version`)은 지우지 않으므로, 엔진을 올리면 그 한 줄이 diff 로 나온다 — 스키마가 바뀐 것이 아니다. "진짜" 헤더를 되찾겠다고 `sed` 하나를 지우면, 그 줄이 눌러 두고 있던 둘 중 하나가 되돌아온다. 읽어 들일 때 손보는 것이 셋 있다. 각각 스키마에 대한 의견 차가 아니라 MySQL 과 H2 의 차이다: [[[#!Table tsv 1 손본 것 왜 외래 키를 `CREATE TABLE` 밖 `ALTER TABLE` 로 덤프는 알파벳 순이라 키가 아직 없는 테이블을 가리키기 일쑤다 — 게다가 `AccessLog` 와 `IpDeny` 는 서로를 참조해서 어떤 순서로도 안 된다 `tinyint(1)` 을 `BOOLEAN` 으로 MySQL 에는 boolean 이 없어 `tinyint(1)` 로 덤프된다. 그대로 두면 H2 가 Integer 를 돌려주고 Boolean 을 기대하는 파서가 전부 실패한다 유니크하지 않은 컬럼으로의 외래 키 제거 MySQL 은 아무 인덱스 접두로의 키를 받아 준다. H2 는 대상이 유니크해야 하고 그러려고 유니크 인덱스를 만든다 — `Page (site, name)` 에 그러면 페이지가 두 번째 revision 을 가질 수 없다 ]]] 셋째 것은 실제 fidelity 손실이다: `Page (site, name)` 으로의 키들은 테스트에서 강제되지 않는다. `TestSchema` 는 그것을 이름이 아니라 대상으로 정한다 — 대상이 유니크하지 않은 키는 전부 빠진다 — 그래서 개수는 덤프가 그때 담고 있는 만큼이지 여기 적어 둘 수는 없다. 2026-08-17 에는 셋이었다: [[[#!Vim bash grep -c 'REFERENCES `Page` (`site`, `name`)' schema/schema.sql ]]] 그 밖의 모든 것 — 모든 `NOT NULL`, default, ENUM 값 목록 — 은 운영이 가진 그대로다. 외래 키의 `ON DELETE` 동작(`CASCADE`·`SET NULL`)도 2026-09-15 부터 그렇다 — 전에는 키를 `ALTER` 로 옮기며 그 절을 떨어뜨려서, 운영에서 cascade 되는 삭제가 테스트에서는 부모를 지우려다 걸렸다. 마지막 문장은 보기보다 무겁다. 덤프는 운영이 아니라 개발 스키마에서 뜨기 때문이다. 아래 `--check` 는 커밋된 파일을 그것이 나온 스키마와 비교한다; 그 스키마를 이 주장의 대상인 운영과 비교하는 것은 없다. 한쪽에만 적용된 evolution 은 스펙이 운영에 없는 모양을 테스트하게 만들고, `--check` 는 여전히 파일이 최신이라고 말한다. '''두 스키마는 결국 같아지지만, 언제인지는 모른다.''' 2026-09-03 실측 — `wiki_aha00a_com` 과 `wiki_aha00a_com_dev` 는 같은 서버의 다른 스키마이고, `Page` 22,748 대 22,747, `Site`·`User` 는 같다. 개발 쪽 `play_evolutions` 의 68·69 행은 '''적용 시각까지 운영과 같다''' — 개발이 운영에서 갱신되며 행을 그대로 물려받았다는 뜻이다. 그래서 evolution 을 배포한 9-02 에는 `--check` 가 그 테이블만큼 차이를 보고했고 하루 뒤에는 통과했다. 갱신이 언제 어떻게 도는지는 이 저장소에 적혀 있지 않으니, '''차이가 사라졌다고 해서 확인이 끝난 것은 아니다''' — 위 문단의 함정은 그대로다. 확인은 덤프 한 번과 `diff` 하나다. 헤더가 맞아떨어지도록 같은 정리를 거치고, 운영 스키마는 운영 저장소가 기록한 호스트와 관리자 자격증명으로 떠서, 커밋된 파일과 비교한다: [[[#!Vim bash ssh "mysqldump --defaults-file= --default-character-set=utf8mb4 \ --no-data --no-tablespaces --column-statistics=0 " \ | sed 's/ AUTO_INCREMENT=[0-9]*//g' \ | sed 's/^-- Dump completed on .*//g' \ | sed 's/Distrib [0-9]*.[0-9]*.[0-9]*, for .*/Distrib #.#.#, for OS/g' \ | sed 's/^-- Host: .*/-- Host: (from local config) Database: (from local config)/' \ | diff schema/schema.sql - ]]] 2026-08-17 실측: 동일. 양쪽 다 24개 테이블 488줄. 두 스키마는 갈라지지 않았고, 갈라지면 이것이 알아내는 방법이다. == 이렇게 만들며 찾은 것 이것이 대체한 손으로 쓴 사본은 운영과 여러 군데가 달랐다 — 정수 폭 아홉, VARCHAR(255) 로 선언된 TEXT 컬럼 둘, 진짜 컬럼이라면 거부할 `targetType` 을 스펙이 넣을 수 있도록 VARCHAR 로 선언된 ENUM 컬럼 셋, evolution 55 가 떨어뜨린 `UserSite` 테이블 하나. 덤프로 바꾸자 이번에는 픽스처에서 더 나왔다: 스펙들이 `abbr` 없는 `Site` 행(운영에서는 `NOT NULL`, default 없음), 뒤에 `Page` 가 없는 `PageMeta` 행, 필수 컬럼 여덟 개가 빠진 `AccessLog` 행을 넣고 있었다. 전부, 그것을 받아 줄 만큼 느슨한 스키마를 상대로 통과해 온 것이다. `UserMergeSpec` 은 운영의 `Page` 에 `User` 로의 외래 키가 없다는 믿음으로, 그 키를 단 자기만의 최소 `Page` 테이블까지 만들어 갖고 있었다. 있다 — `Page_User_seq_fk` — 그래서 대역은 불필요했고 스펙은 이제 진짜 테이블을 쓴다. == 알려진 구멍 * 스키마를 덤프 대신 evolution 에서 만들면 테스트가 마이그레이션 자체에 묶인다. 그런데 안 된다: 2026-08-10 실측으로 67개 파일 중 '''17개가 실패했다 — 41문장''' — 그 뒤 들어온 69 도 `MODIFY … COLLATE` 에서 걸린다. H2 가 MySQL 모드에서도 거부하는 MySQL 문법이다. [[[#!Table tsv 1 문법 예 컬럼 위치 지정 `ALTER TABLE Page MODIFY comment TEXT NOT NULL AFTER remoteAddress` `... FIRST` `ALTER TABLE Link ADD site INT DEFAULT 1 NOT NULL FIRST` 한 문장에서 키를 지우고 더하기 `ALTER TABLE Page DROP PRIMARY KEY, ADD PRIMARY KEY (site, name, revision)` `TABLE` 없는 `TRUNCATE` `TRUNCATE TermFrequency` MySQL 날짜 함수 `UPDATE` 안의 `DATE_ADD(...)` ]]] `ALTER` 만이 아니다 — `TRUNCATE`, `rename table`, 데이터를 고치는 `UPDATE` 도 있다. 덤프에는 하나도 없다 — `CREATE TABLE` 뿐이다 — evolution 이 안 되는 곳에서 덤프가 되는 이유가 그것이다. * `schema/schema.sql` 갱신은 여전히 손으로 하는 단계이고, 아무도 덤프하지 않은 스키마 변경은 스펙을 어제의 모양으로 테스트하게 놔둔다. 실패 양상은 전보다 순하다 — 스펙마다 제각각의 발명품 대신 스위트 전체가 하나의 낡은 스키마에서 돈다 — 하지만 그것을 일어나게 만드는 것은 없다. 지금 있는 것은 물어보는 방법이고, 초록 스위트를 믿기 전에 돌려 볼 만큼 싸다: [[[#!Vim bash sh ./schemaDump.sh --check ]]] 라이브 스키마를 같은 정리를 거쳐 덤프해 커밋된 파일과 diff 하고, 다르면 그 차이와 함께 0 아닌 코드로 끝난다. `schema/schema.sql` 은 건드리지 않는다(임시 파일 `schema/.schema.sql.tmp` 를 썼다가 지운다). 두 경로가 한 함수에서 나오는 것은 일부러다: 그 파이프라인의 사본 둘은 `sed` 하나씩 갈라져, 검사가 있지도 않은 차이를 보고하기 시작한다. 빌드에 물려 놓지는 않았다. 로컬 설정과 거기 적힌 데이터베이스가 필요해서, 자격증명 없는 기계의 테스트 실행이 엉뚱한 이유로 실패하게 된다.