37장. 관례가 없는 코드베이스에서 — 자체 프레임워크와 계층 미분리
지금까지의 예시에는 조용한 전제가 하나 있었다.
Spring이라는 것.
@RestController 를 보면 Agent는 무엇인지 알고,
JPA 엔티티를 읽으면 스키마를 안다.
이 사전 지식 위에 이 책의 상당 부분이 서 있다.
그런데 이런 코드베이스가 있다.
자체 프레임워크 (10년 전 사내 개발)
Controller 에 비즈니스 로직이 그대로
데이터 접근은 자체 쿼리 빌더
계층이라 부를 만한 것이 없음
드문 상황이 아니다. 그리고 앞의 전제가 깨진다.
무엇이 달라지는가
34~36장은 그대로 통한다.
진입점 조사, 지도, 특성화 테스트는 프레임워크와 무관하다.
문제는 이 셋이다.
| 장 | 책의 전제 | 이 상황 |
|---|---|---|
| 26장 | 최근에 잘 만든 API를 지목한다 | 지목할 좋은 예시가 없다 |
| 27장 | JPA 엔티티가 스키마 문서다 | 스키마가 코드에 안 드러난다 |
| 40장 | 계층을 정리한다 | 계층이 없다. 신설이다 |
그리고 책이 아예 다루지 않은 문제가 하나 더 있다.
가장 큰 문제 — Agent가 아는 척한다
⚠️ 이것이 이 장의 핵심이다.
Agent는 모르면 멈추지 않는다.
비슷하게 생긴 것의 관례를 가져다 쓴다.
// 우리 쿼리 빌더
Query.from("orders").where("status", "=", "PAID").list()
// Agent가 QueryDSL 관례로 쓴 것
Query.from("orders").where(order.status.eq("PAID")).fetch()
이건 컴파일 에러라 차라리 낫다.
🔥 무서운 것은 이름은 같은데 동작이 다른 경우다..limit(10) 이 우리는 페이지당 개수인데
다른 라이브러리 관례로는 전체 상한이라면, 아무도 모른다.
DI도 같다. @Autowired 를 붙였는데
우리 컨테이너의 주입 시점이 다르면 운영에서 NPE가 난다.
방어 1 — “이건 무엇이 아니다” 를 먼저 쓴다
14장에서 CLAUDE.md 에 무엇을 적을지 다뤘다.
이 상황에서는 아닌 것을 먼저 적는다.
# order-service
⚠️ 이 프로젝트는 Spring 을 쓰지 않는다.
자체 프레임워크(`core/`)와 자체 쿼리 빌더(`core/query/`)를 쓴다.
## 절대 가정하지 말 것
- Spring 애노테이션은 존재하지 않는다. 쓰면 컴파일되지 않거나 무시된다
- JPA·Hibernate 는 없다
- 쿼리 빌더는 QueryDSL·jOOQ 와 이름만 비슷하다.
용법은 `docs/query-builder.md` 를 반드시 확인한다
- 모르는 API 는 추측하지 말고 `core/` 에서 실제 정의를 찾아 확인한다
마지막 줄이 핵심이다.
15장에서 “덕목은 규칙이 아니다” 라고 했는데
이건 덕목이 아니라 판정 가능한 행동 지시다.
core/ 를 열어봤는지는 도구 호출 기록에 남는다.
방어 2 — 프레임워크 레퍼런스를 만든다
문제의 뿌리는 참고할 문서가 없다는 것이다.
Spring은 학습 데이터에 있지만 우리 프레임워크는 어디에도 없다.
만들어야 하고, 이것도 Agent가 돕는다.
core/query/ 의 쿼리 빌더 API 문서를 만들어줘.
- public 메서드 전부와 시그니처
- 각 메서드가 실제로 생성하는 SQL (구현을 읽고 확인해줘)
- 이름이 비슷한 공개 라이브러리와 동작이 다른 부분
추측하지 말고 구현 코드에서 확인한 것만 적어줘.
확인 안 되는 건 "확인 필요" 로 남겨줘.
⚠️ 마지막 두 줄이 없으면 QueryDSL 문서를 재현해온다.
🔥 이 문서 하나가 투자 대비 효과가 가장 크다.
없으면 매 세션 같은 실수가 반복된다.
방어 3 — 자주 틀리는 대비표
15장의 “자주 틀리는 것만 남긴다” 를
이 상황에 맞게 쓰면 대비표가 된다.
| Agent가 쓰려는 것 | 우리 프로젝트에서는 |
|---|---|
@Transactional | Tx.run { } 블록 |
@Autowired | 생성자 파라미터 |
repository.findById(id) | Query.from("t")...first() |
.fetch() | .list() |
사고가 날 때마다 한 줄씩 늘어난다.
16장의 개선 루프가 여기서는 훨씬 빨리 돈다.
참고할 좋은 예시가 없을 때
26장의 핵심 전략이 무력화된다.
가장 최근에 잘 만든 것을 지목하라
그런 파일이 없으면 하나를 만든다.
새 요구사항 중 가장 작은 것을 골라
사람이 목표 구조대로 직접 구현하고, 기준으로 지정한다.
## 기준 구현
신규 코드는 아래를 본보기로 삼는다.
- `order/CreateOrderHandler.kt` — 핸들러 구조
- `order/OrderService.kt` — 로직 분리
- `order/OrderQuery.kt` — 데이터 접근
⚠️ 위 세 파일 외의 기존 코드는 본보기가 아니다.
⚠️ 이때 Agent를 쓰지 않는 것이 중요하다.
기준이 될 파일은 관례가 없는 상태에서 만들어진다.
Agent가 만들면 다른 프레임워크의 관례가 섞이고,
그것이 이후 모든 코드에 복제된다.
계층이 아예 없을 때
40장은 계층을 재배치하는 이야기였다.
여기서는 신설이라 규모가 다르다.
⚠️ 전면 재배치는 실패한다.
파일 400개를 옮기는 동안 기능 개발이 멈춘다.
handler/ service/ query/ ← 새 구조 (신규 코드만)
legacy/ ← 기존 전부 (건드리지 않음)
- 기존 파일을 수정할 일이 생기면
그 파일의 비즈니스 로직만 service/ 로 분리한다
- 분리 전에 그 경로의 특성화 테스트를 먼저 만든다 (36장)
- 한 번에 한 파일만. 옮기는 김에 주변을 정리하지 않는다
그리고 신구조 비율을 지표로 본다. 4% → 17% → 31%.
🔥 이 숫자가 있으면 “언제 끝나냐” 에
“이 속도면 2년” 이라고 답할 수 있다.
스키마를 어떻게 파악시키는가
27장은 JPA 엔티티를 스키마 문서로 썼다.
여기서는 테이블·컬럼이 문자열로 흩어져 있다.
Query.from("orders").select("id", "user_id", "total_amount")
DDL 덤프와 코드 문자열을 대조하는 편이 낫다.
코드에서 참조하는 테이블·컬럼 문자열을 전부 추출해줘.
그다음 실제 DDL과 대조해서 세 가지로 나눠줘.
- 코드에도 있고 DB에도 있다
- 코드에는 있는데 DB에 없다 (⚠️ 오타 또는 죽은 코드)
- DB에는 있는데 코드에서 안 쓴다 (죽은 컬럼 후보)
⚠️ 두 번째 목록이 나오면 대개 놀란다.
문자열이라 컴파일러가 안 잡아준다.
조건 분기 안에 있으면 몇 년간 아무도 모른다.
35장의 데이터 소유권 표는 이 결과 위에서 만든다.
아키텍처 테스트를 어떻게 거는가
42장의 의존성 테스트는 패키지 참조를 검사하므로
프레임워크와 무관하게 쓸 수 있다.
⚠️ 그런데 이 상황의 핵심 문제는 안 잡힌다.
“Controller에 비즈니스 로직이 있다” 는
의존성 규칙으로 표현되지 않는다.
증상으로 대신 잡는다.
@Test fun `handler 메서드는 40줄을 넘지 않는다`()
@Test fun `handler 에서 Query 를 직접 만들지 않는다`() // 문자열 검사
@Test fun `handler 의 조건 분기는 5개를 넘지 않는다`()
셋 다 거칠다.
로직이 있는지가 아니라 로직이 있을 때 나타나는 증상을 본다.
🔥 그래도 효과가 있다.
핸들러에 로직을 쓰기 시작하면 길이나 분기 수에서 걸린다.
42장의 baseline 방식으로
기존 위반을 예외 목록에 넣고 시작한다.
우선순위가 바뀐다
39장의 축을 그대로 쓰되 가중치가 달라진다.
| 작업 | 일반적인 경우 | 이 상황 |
|---|---|---|
| 특성화 테스트 | 중요 | 🔥 최우선 |
| 프레임워크 레퍼런스 | 불필요 | 🔥 최우선 |
| 기준 구현 하나 | 불필요 | 높음 |
| 경계 찾기 | 최우선 | 나중에 |
⚠️ 경계 작업을 미루는 것이 맞다.
38장에서 “경계는 코드가 알려준다” 고 했는데,
그 코드가 아무 말도 하지 않는 상태다.
안전망(특성화 테스트) → 언어(레퍼런스) → 본보기(기준 구현)
→ 방벽(길이·분기 규칙) → 축적(새 코드부터) → 그다음에 경계
그래도 나아지는 것
| 얻는 것 | 이유 |
|---|---|
| 문서가 처음 생긴다 | 레퍼런스는 사람에게도 없던 것 |
| 죽은 코드·컬럼이 드러난다 | 문자열 대조의 부수 효과 |
| 기준이 생긴다 | 10년 만에 “이렇게 쓰자” 가 정해진다 |
| 신규 입사자 학습이 빨라진다 | 그 문서가 온보딩 자료 |
🔥 마지막 줄이 크다.
자체 프레임워크 프로젝트의 가장 큰 비용은
사람이 적응하는 시간이고, 그 레퍼런스가 그것을 직접 줄인다.
4장에서 말한 그대로다.
사람에게 관행으로 남겨둔 암묵지를
Agent에게는 파일로 꺼내야 한다.
관행조차 없던 프로젝트에서는
그 작업이 곧 관행을 만드는 일이 된다.
이 장의 핵심
- 이 책의 예시는 Spring이라는 전제 위에 있고, 자체 프레임워크에서는 깨진다
- 34~36장의 조사·지도·특성화 테스트는 프레임워크와 무관하게 통한다
- Agent는 모르면 멈추지 않고 비슷하게 생긴 것의 관례를 가져다 쓴다
- 이름은 같은데 동작이 다른 API가 가장 위험하다
CLAUDE.md첫 줄에 “이것은 무엇이 아니다” 를 적는다- 프레임워크 레퍼런스 문서가 투자 대비 효과가 가장 크다
- 참고할 좋은 예시가 없으면 사람이 하나를 직접 만들어 기준으로 삼는다
- 기준 구현을 Agent에게 맡기면 다른 관례가 섞여 이후 전부에 복제된다
- 계층이 없으면 재배치가 아니라 신설이다 — 새 코드부터, 비율을 지표로
- 테이블·컬럼 문자열과 DDL을 대조하면 죽은 코드와 오타가 드러난다
- 의존성 규칙으로는 “핸들러에 로직 있음” 을 못 잡는다 — 길이·분기 수로 대신한다
- 경계 작업은 뒤로 미룬다. 코드가 아무 말도 하지 않는 상태이기 때문이다