- analysis/·source-index·state.json 을 final/document.md 제2부·제3부로 접었다. SSOT 는 하나다 - 파일럿 — commit-ambiguity-as-a-result 를 새 기준으로 재선별. 후보 14 → 글감 5 (PROMOTE 5 · MERGE_INTO 3 · KEEP_IN_SSOT 4 · 보류 2). 기록 5건을 다시 썼고 그림 1개를 techviz 로 만들었다 - 재선별이 잡은 것: 제1부 §6.2·§11.1 이 자기 §13.2 와 어긋나 있었다(레인을 안 돌렸다 vs 돌렸다) — 정정. 이미 답이 나와 있던 Question 을 HEAD 재실행 질문으로 다시 세웠다. Concept 이 인용한 코드가 SSOT 에 없어 뺐다 - candidateScope·sourceRepository 기록. 나머지 43개 주제는 재선별 대기(PENDING 905) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
126 lines
8.0 KiB
Markdown
126 lines
8.0 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: a-build-gate-that-is-not-in-the-build
|
|
title: '"build gate"라 불리는 catalog drift 검사가 어디에서도 실행되지 않는다'
|
|
topic: redis-command-admission
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:a-build-gate-that-is-not-in-the-build
|
|
evidenceCapturedOn: 2026-09-02
|
|
assets:
|
|
- key: a-build-gate-that-is-not-in-the-build
|
|
file: ../../../final/evidence/rendered/a-build-gate-that-is-not-in-the-build.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/a-build-gate-that-is-not-in-the-build.txt
|
|
source:
|
|
- 원본 분석 절은 final/document.md#a10 §47 · final/document.md#4-4 이다. 참조 수는 §8.1 의 원문 측정과 같다.
|
|
- Gradle 태스크와 CI 단계가 없다는 것은 이 기록에 붙은 자산에 있다. 토폴로지 레인의 두 설정은 final/document.md#a10 §0 과 evidence/raw/158-cache-redis-module-inventory.txt 에, 레인의 결합 상태는 `cache-redis/build.gradle` 과 `.github/ci-gate-matrix.yml` 에 있다.
|
|
module: adapter-outbound-cache-redis
|
|
priority: P2
|
|
---
|
|
|
|
# "build gate"라 불리는 catalog drift 검사가 어디에서도 실행되지 않는다
|
|
|
|
명령 메타데이터 드리프트를 검사하는 코드가 있고 정책 파일 머리 주석이 그것을 빌드 게이트라고 부른다. 그 검사를 실행하는 Gradle 태스크도 CI 단계도 없다.
|
|
|
|
## 관계
|
|
|
|
- **산문이 선언한 게이트는 빌드에 있는 게이트가 아니다**
|
|
이 사례가 그 규칙을 만든 형태다.
|
|
- **두 파일이 같은 검증기를 "빌드를 실패시키는 것"이라 적고, 어떤 빌드도 그것을 부르지 않는다**
|
|
같은 형태가 gRPC 가족에서 나타난 사례다.
|
|
- **서버 메타데이터가 명령의 정의이고 정책 파일은 허용 범위다**
|
|
이 검사가 지키려는 관계다.
|
|
|
|
## 문제
|
|
|
|
명령 카탈로그의 정의는 서버 메타데이터에서 온다. 정책 파일은 그 위에서 허용 범위를 정한다.
|
|
|
|
두 쪽이 어긋날 수 있다. 서버 버전이 올라가면서 명령의 키 스펙이나 플래그가 바뀌면 정책 파일이 옛 정의 위에 서 있게 된다.
|
|
|
|
그 드리프트를 검사하는 코드가 있다. 정책 파일 머리 주석이 그것을 빌드 게이트라고 부른다.
|
|
|
|
## 결론
|
|
|
|
빌드에 없다.
|
|
|
|
그 검사를 실행하는 Gradle 태스크가 등록되어 있지 않고, CI 워크플로에도 그것을 부르는 단계가 없다. 그 코드는 누군가 수동으로 부를 때만 돌고, 부르는 절차는 문서에 없다.
|
|
|
|
이름은 게이트인데 빌드에서 그것을 부르는 곳이 없다.
|
|
|
|
## 검증 환경
|
|
|
|
OpenJDK : 21.0.12
|
|
Gradle : 9.0.0
|
|
확인 방식 : 검사 코드 확인과 태스크 및 CI 단계 이름 검색
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
1. 명령 메타데이터 드리프트 검사 클래스를 확인한다.
|
|
2. 정책 파일 머리 주석에서 그것을 부르는 이름을 확인한다.
|
|
3. 그 검사를 실행하는 Gradle 태스크를 찾는다.
|
|
4. CI 워크플로에서 그 검사를 부르는 단계를 찾는다.
|
|
|
|
1~2 단계의 원문 근거는 final/evidence/raw/163-cache-redis-guard-connection-codec-probes.txt §8.1 이다. 3~4 단계의 출력은 이 기록에 붙은 자산에 있다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
cache-redis 어댑터의 명령 정책 파일이 머리 주석에서 이 저장소에서 가장 강한 governance 주장을 한다 — 명령이 무엇인지는 서버 메타데이터가 정하고, 이 SDK 가 그것으로 무엇을 할지는 이 파일이 정하며, **"드리프트 게이트가 둘을 비교해 서버가 이 파일이 판정하지 않은 명령을 추가하면 빌드를 실패시킨다"**. `RedisCommandMetadataDiff` 의 javadoc 도 자신을 "The build gate" 라고 부른다.
|
|
|
|
비교 로직은 완성돼 있다. 테스트 여섯 개가 다섯 버킷을 덮는다.
|
|
|
|
## 이 이름을 참조하는 파일이 둘뿐이다
|
|
|
|
:::evidence key="a-build-gate-that-is-not-in-the-build" alt="코드베이스에서 RedisCommandMetadataDiff 를 검색한 출력 9줄. 이 이름을 참조하는 파일이 선언 자신과 자기 단위 테스트 둘뿐이고, 이 비교를 실행하는 Gradle 태스크와 CI 단계가 없다는 것이 그 출력에 그대로 보인다." caption="RedisCommandMetadataDiff 참조 · Gradle 태스크 · CI 단계 — 9줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
선언 자신과 자기 단위 테스트. 이 비교를 실행하는 Gradle 태스크가 없고, CI 워크플로에서 부르는 단계도 없다. 실제 서버 메타데이터를 이 함수에 넣는 코드가 저장소에 없다.
|
|
|
|
빌드를 깨는 게이트는 존재하지 않는다. 존재하는 것은 게이트가 쓸 비교 함수와 그 함수의 단위 테스트다.
|
|
|
|
## 다섯 버킷이 막기로 되어 있던 것
|
|
|
|
1. 아무도 분류하지 않은 새 명령
|
|
2. 서버에서 사라진 명령
|
|
3. 키 추출이 엉뚱한 인자를 가리키게 만드는 key spec 이동
|
|
4. 계정을 조용히 넓히는 ACL 카테고리 변경
|
|
5. 타입 있는 API 가 아직 노출하는 deprecation
|
|
|
|
여섯 번째 테스트는 버킷이 아니라 드리프트가 없을 때를 고정하는 음성 케이스다.
|
|
|
|
## 첫 버킷은 다른 장치가 대신 막는다
|
|
|
|
새 명령이 조용히 통과하지는 않는다. `RedisCommandCatalog.require` 가 분류되지 않은 명령을 fail-closed 로 거부하고, `theCatalogFailsClosedForAnUnclassifiedCommand` 가 그것을 고정한다. 그래서 이 결함의 데이터 위험이 즉각적이지 않다.
|
|
|
|
원본 분석이 위험으로 지목한 것은 3·4·5 번 셋이다. key spec 이 이동하면 이 SDK 의 네임스페이스·슬롯 검사가 잘못된 인자를 키로 본다. ACL 카테고리가 넓어지면 계정 분리 가정이 조용히 약해진다. deprecation 은 타입 있는 API 가 사라질 명령을 계속 노출하게 둔다.
|
|
|
|
2 번(서버에서 사라진 명령)은 어느 쪽으로도 논의되지 않았다. 카탈로그가 그것을 잡는다는 근거도, 위험 목록에 든다는 근거도 원본에 없다.
|
|
|
|
## 게이트라는 이름이 정책 파일의 주의 수준을 낮춘다
|
|
|
|
정책 파일을 읽는 사람이 그 주석을 보고 드리프트가 자동으로 잡힌다고 이해할 수 있다 — 이것은 원본 분석에 적힌 판정이 아니라 이 기록의 추론이다. 다만 주석의 문장이 조건 없는 단정이라는 것은 사실이고, 그 문장을 읽고 나서 정책 파일을 손볼 때 확인해야 할 것이 하나 줄어든다.
|
|
|
|
## 이 리프의 토폴로지 레인과 비교하면
|
|
|
|
같은 리프에 `redisTopologyTest` 레인이 있다. 그 레인은 `failOnNoDiscoveredTests = true` 와 `outputs.upToDateWhen { false }` 를 갖는다 — 발견 0 이 성공이 되지 않고, 이전 실행 결과를 다시 내놓지도 않는다.
|
|
|
|
다만 그 레인도 릴리스 게이트 Gradle 태스크에 묶여 있지는 않다. 기본 `test` 는 `excludeTags 'redis-topology'` 로 그것을 제외하고, 실행하는 것은 CI 워크플로 하나다. gate matrix 에서 그 항목은 `mechanism: workflow-job` 이고 `release_blocking: conditional` 이다 — 같은 매트릭스의 `httpclient-stable-contract` 가 `gradle-custom-task` 에 `release_blocking: true` 인 것과 다르다.
|
|
|
|
드리프트 검사에는 그 둘 중 아무것도 없다. 레인도 아니고 워크플로 항목도 아니다.
|
|
|
|
## 없는 것은 연결 한 줄이다
|
|
|
|
토폴로지 레인은 이미 실제 서버에 붙어 있고 비교 함수도 완성돼 있다. 그 레인에서 `COMMAND DOCS` 와 `COMMAND INFO` 를 읽어 `RedisCommandMetadataDiff.compare(...)` 를 돌리고, 결과가 비어 있지 않으면 실패시키면 된다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
지금 드리프트가 나 있는지는 직접 돌려 보지 않았다. 그 검사를 부르는 실행 경로가 없다는 것까지만 확인했다.
|
|
|
|
태스크와 CI 단계의 부재는 이름 기반 검색으로 판정했다. 리플렉션이나 서비스 로더처럼 이름이 문자열로만 등장하는 호출 형태는 배제하지 못했다.
|
|
|
|
<!-- body:end -->
|