기술 블로그 글에서 주장한 동작을 독자가 직접 재현할 수 있게 만든 최소 예제 모음이다. 각 모듈은 verify.sh 하나를 실행하면 글이 주장한 결과가 그대로 나온다. 이 레포의 목적은 코드를 보여주는 게 아니라 주장을 검증 가능하게 만드는 것이므로, 의존성은 검증에 필요한 최소한만 두고 각 모듈은 다른 모듈에 의존하지 않는다.
모듈의 단위는 글이 아니라 주장 하나다. 보통은 글 한 편에 모듈 하나가 대응하지만, 한 글이 통제 조건이 다른 실험 두 개를 요구하면 모듈을 나눈다. 001과 002가 그런 경우다. 001은 컴파일러 플러그인만 남기려고 Hibernate를 일부러 뺐고, 002는 실제 기동을 봐야 해서 Hibernate가 필요하다. 한 모듈에 넣으면 둘 다 흐려진다.
| 번호 | 주제 | 글 | 모듈 | 핵심 주장 |
|---|---|---|---|---|
| 001 | Kotlin JPA 엔티티의 final |
@Entity는 지울 수 없다 | 001-jpa-entity-final |
@Entity는 no-arg 생성자를 합성하지만, final 해제는 Kotlin 2.3.20부터다 |
| 002 | Java 엔티티의 XML 전용 매핑 | @Entity는 지울 수 없다 | 002-java-entity-xml-only |
Java는 어노테이션 0개로도 orm.xml만으로 매핑된다 (001의 대조군) |
| 003 | metadata-complete의 한계 |
orm.xml로 옮길 수 있는 것과 없는 것 | 003-metadata-complete |
어노테이션을 무시시킬 뿐, XML에 빠진 선언을 채워주지 않는다 |
각 모듈 디렉터리에서 verify.sh를 실행한다.
cd 001-jpa-entity-final
./verify.shJDK 17 이상만 있으면 된다. Gradle은 wrapper가 받아온다.
전체 모듈 목록은 아래로도 볼 수 있다.
./gradlew modulesblog-lab/
├── README.md # 이 파일. 전체 목차
├── settings.gradle.kts # 모듈 등록 + Kotlin 버전 스위칭 지점
├── build.gradle.kts # 공통 설정만
├── gradle.properties # kotlinVersion (SSOT)
├── gradle/libs.versions.toml # 라이브러리 버전 카탈로그
└── NNN-slug/ # 글 1편 = 모듈 1개
├── README.md # 검증하는 주장 / 실행법 / 실제 출력
├── build.gradle.kts
├── verify.sh # 이걸 실행하면 결론이 나온다
└── src/
모듈 이름은 NNN-slug 형식이다. NNN은 추가 순서, slug는 검증하는 주장을 가리킨다. 글 URL과 같을 필요는 없다 — 한 글에 모듈이 여럿 붙기 때문이다.
NNN-slug/디렉터리를 만든다.settings.gradle.kts의include(...)에 한 줄 추가한다.- 모듈
README.md에 검증하려는 주장을 먼저 한 문단으로 쓴다. verify.sh를 쓰고, 실제 실행 출력을 모듈 README에 붙인다.- 위 목차 표에 행을 하나 추가한다. 모듈 링크는 상대경로(
./NNN-slug)로 쓴다.
- 각 모듈은 독립 실행 가능하다. 다른 모듈에 의존하지 않는다.
- 의존성은 검증에 필요한 최소한만. 변수가 늘면 무엇이 원인인지 말할 수 없게 된다.
- 코드보다 README가 중요하다. 뭘 왜 검증하는지가 명확해야 한다.
- 실행 결과를 재현할 수 있는 게 이 레포의 전부다. 재현이 안 되면 그 모듈은 실패한 것이다.
이 레포의 모듈과 스크립트는 AI 에이전트(Claude Code)로 작성했다. 그래서 층을 나눠 둘 필요가 있다.
| 누가 만들었나 | 무엇이 보증하나 | |
|---|---|---|
| 검증할 주장 | 사람. 실무에서 겪은 것 | — |
관찰 장치 (verify.sh, 모듈 코드) |
AI | 아무것도. 읽고 검토해야 한다 |
관찰 결과 (javap 출력, Hibernate 기동) |
JDK, Hibernate | 도구 자체 |
가운데 줄만 의심하면 된다. "Kotlin 2.3.20부터 final이 풀린다"는 AI의 주장이 아니라 컴파일러가 만든 바이트코드이고, "어노테이션 0개로 매핑된다"는 Hibernate가 실제로 INSERT를 날렸다는 뜻이다. AI가 개입할 여지가 없는 층이다.
무너질 수 있는 지점은 하나다. 관찰 장치가 엉뚱한 것을 보고 있는 경우. 001의 verify.sh가 javap -v 출력에서 -m1을 쓰는 이유(뒤에 나오는 flags:는 필드/메서드 것이다)나, 002가 exclude-unlisted-classes로 클래스패스 스캔을 끄는 이유가 그래서 주석으로 남아 있다. 이 한 줄들이 빠지면 그럴듯하지만 틀린 결과가 나온다.
그래서 위 원칙 중 "의존성은 최소한만"과 "코드보다 README가 중요하다"가 여기서 안전장치 역할을 한다. 스크립트가 사람이 읽어서 검토할 수 있는 크기로 남아 있어야 이 구조가 성립한다. 모듈이 늘어나 검토되지 않은 스크립트가 섞이는 순간 이 문단은 거짓이 된다.
버전에 따라 동작이 달라지는 것을 보여주는 모듈이 있으므로, Kotlin 버전은 한 곳에서 갈아끼울 수 있게 해뒀다.
./gradlew build # gradle.properties의 kotlinVersion
./gradlew build -PkotlinVersion=2.2.20 # 오버라이드값은 settings.gradle.kts의 pluginManagement가 읽는다. 플러그인 해석보다 먼저 평가돼야 하므로 버전 카탈로그가 아니라 gradle.properties에 둔다.