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