docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
@@ -0,0 +1,552 @@
|
||||
# 99 · 교차 스코프 분석 — 사이클 2
|
||||
|
||||
> 상태: COMPLETE
|
||||
> 기준 revision: `21234e38cdb9a926cbc92bb97a2aee2e4a7d2916`
|
||||
> 분모: 등록 리프 **62** (COMPLETE 61 · EXCLUDED 1 `sample-portfolio`)
|
||||
> 근거: 각 리프 SSOT 문서 61건 · `evidence/raw/` 370건
|
||||
> 사이클 1의 같은 경로에 초안이 있었고(364줄, 상태 `NOT_STARTED`), 이 문서가 그것을 대체한다.
|
||||
> 초안은 보존하지 않았다 — 덮어쓰기 전에 읽지 않은 것은 이 사이클의 절차 실수다.
|
||||
> 따라서 **사이클 1의 절 번호와 줄 번호는 이 문서에 적용되지 않는다.** `root-tree.md`가 옛 번호로
|
||||
> 걸어 둔 앵커는 이 문서의 절로 다시 걸었다(§3.1 · §3.4 · §5 · §6 · §2·§4).
|
||||
> 옛 초안에만 있던 서술이 있었다면 그것은 복구되지 않았다. 다만 그 초안이 인용하던 1차 근거는
|
||||
> 전부 리프 SSOT와 `final/document.md`에 있으며, 이 문서는 그것들을 다시 읽고 작성했다.
|
||||
> **사이클 2 후반 갱신.** 이 문서의 §0~§6 은 18개 리프 재검증까지를 반영해 쓰였다. 그 뒤 같은 사이클에서
|
||||
> 23개 리프(messaging 5 · grpc 18)의 production 구현을 통독했고, 그 결과를 §1.2 · §3.7 · §4 의
|
||||
> 6~9항 · §6 에 더했다.
|
||||
>
|
||||
> **그 통독 자체를 다시 했다 — 이 문서의 §0 · §1.2 · §3.5 · §6 숫자가 그래서 바뀌었다.** 앞선 판에서
|
||||
> 23개 리프를 `FULL_READ_DONE` 으로 표시하고 "395파일 전수 통독" 이라고 적었으나, 실제로 읽은 것은 리프마다
|
||||
> 일부였다. 그 표시가 사실이 아니었으므로 23개 리프를 파일 단위로 다시 세고 처음부터 다시 읽었다. 정직한
|
||||
> 분모는 **main 357파일 / 29,542줄 · test 121파일 / 19,756줄**이고,
|
||||
> 각 리프 SSOT 의 Coverage ledger 를 그 숫자로 다시 썼다. §1.2 의 결론 하나가 그 과정에서 뒤집혔다 —
|
||||
> "통독이 만든 새 finding 에 P1 은 없다" 는 서술이 더 이상 참이 아니다.
|
||||
|
||||
> 지위: 이 문서는 **2차 증거**다. 리프의 사실은 리프 SSOT가 소유하고, 여기서는 리프 경계를 넘을 때만 성립하는 것을 다룬다.
|
||||
|
||||
---
|
||||
|
||||
## 0. 이 문서가 서 있는 분모
|
||||
|
||||
| 항목 | 수 |
|
||||
|---|---:|
|
||||
| 등록 리프 | 62 (COMPLETE 61 · EXCLUDED 1) |
|
||||
| `analysisFile`을 공유하는 리프 | **0** — SSOT 게이트 통과 |
|
||||
| 리프 SSOT가 확정한 finding | **462** (P1 30 · P2 147 · P3 285) — 아래 주 참조 |
|
||||
| finding 0건인 리프 | 8 |
|
||||
| evidence 파일 | 370 |
|
||||
| 끊긴 evidence 참조 | **0** |
|
||||
| 사이클 2 전수 통독 리프 | **23** (messaging 5 · grpc 18) |
|
||||
| 그 23개 리프의 main production | **357파일 / 29,542줄** |
|
||||
| 그 23개 문서의 finding | **100** (P1 2 · P2 31 · P3 67) |
|
||||
| 그중 사이클 1 가족 문서에서 옮겨온 것 | 8 |
|
||||
| 통독에서 처음 나온 것 | **92** (P1 2 · P2 25 · P3 65) |
|
||||
|
||||
**두 숫자의 근거가 다르다.**
|
||||
|
||||
23개 리프의 100건은 이번에 직접 센 것이다 — 23개 문서의 §17 에서 `### 17.n P<k> —` 형태를 파싱했고,
|
||||
23개 문서 전부를 처음부터 끝까지 읽은 뒤이므로 표기 누락이 없다. 앞선 판의 60건은 통독이 실제로는
|
||||
부분 통독이던 시점의 수치다.
|
||||
|
||||
전체 462건은 **직접 다시 센 것이 아니라 델타로 조정한 값**이다. 앞선 판의 422건에서 23개 리프 몫 60을
|
||||
빼고 100을 더했다(422 − 60 + 100 = 462). 나머지 38개 문서는 이번 재작업의 대상이 아니었고, 그 문서들이
|
||||
쓰는 「모듈 findings 표」 형식은 §17 형식과 파싱 규칙이 달라 두 형식을 함께 세는 스크립트를 이번에
|
||||
다시 돌리지 않았다. 그러므로 **462는 델타가 정확하다는 가정 위에 있고, 422 자체의 재측정은 아니다.**
|
||||
직접 재측정이 필요하면 두 형식을 모두 파싱하는 원래 스크립트를 61개 문서에 다시 돌려야 한다.
|
||||
|
||||
P1·P2·P3 내역도 같은 방식으로 조정했다 — P1 29−1+2=30, P2 135−19+31=147, P3 258−40+67=285.
|
||||
|
||||
---
|
||||
|
||||
## 1. 사이클 2가 실제로 바꾼 것
|
||||
|
||||
사이클 2의 재검증 대상은 사이클 1이 남긴 18개 리프 문서였다. 결과는 다음과 같다.
|
||||
|
||||
**소스는 움직이지 않았다.** 18개 문서가 모두 기준으로 삼은 `a24ece9c`와 현재 HEAD `21234e38` 사이는
|
||||
커밋 하나이고, 그 커밋은 `src/grpc/**`·`src/grpc-advanced/**`와 공통 파일 둘만 건드렸다. 18개 리프 경로의
|
||||
변경 파일 수는 전부 **0**이다. 공통 파일 둘도 이 18개에 영향이 없다 — `src/build.gradle`의 변경은
|
||||
plain-JUnit 테스트 클래스패스 조건에 `:grpc:`·`:grpc-advanced:`를 더한 것뿐이고,
|
||||
`modules.json`은 197줄 순수 추가로 18개 리프 id가 diff에 한 번도 등장하지 않는다 (`EVD-333`).
|
||||
|
||||
**그래서 재검증의 실질은 재작성이 아니라 재확인이었다.** 18개 문서의 lane을 HEAD에서 다시 돌렸고
|
||||
(`EVD-334`), 실패 5건이 나왔다. 그 5건에 대한 판정은 이렇다.
|
||||
|
||||
| 리프 | 실패 | 사이클 1의 판정 | 사이클 2의 재측정 |
|
||||
|---|---|---|---|
|
||||
| `adapter-outbound-fileserver` | 1 | 환경(로케일) | **확인** — `LANG=C.utf8`로 통과, `sun.jnu.encoding` ANSI→UTF-8 |
|
||||
| `app-bootstrap` | 1 | 환경(`jq` 부재) + 가드 비대칭 P3 | **확인**, 그리고 남은 공백을 메움 — 15개 레인 계약을 독립 경로로 검증 |
|
||||
| `adapter-outbound-httpclient` | 3 | **P1 제품 결함** | **철회** — 픽스처의 듀얼스택 호스트명이 원인 |
|
||||
|
||||
세 번째가 이 사이클의 유일한 판정 번복이다. 사이클 1은 `ApacheFailureClassifier`의 분기 순서를 읽고
|
||||
"Apache가 TLS 실패를 `HttpHostConnectException`으로 감싸므로 CONNECT 분기가 TLS 분기를 가린다"고
|
||||
결론했다. 예외 사슬을 실제로 출력해 보면 그 사슬에 `SSLHandshakeException`이 **없다**. 원인은
|
||||
`MockHttpServer.uri()`가 호스트명 `localhost`를 돌려주는데 이 컨테이너의 `localhost`가 `127.0.0.1`과
|
||||
`::1` 양쪽으로 풀리고 `MockWebServer`는 IPv4에만 바인딩한다는 것이었다. Apache의 다중 주소 루프가
|
||||
첫 주소(127.0.0.1)의 진짜 TLS 실패를 삼키고 마지막 주소(::1)의 연결 거부만 승격시킨다. 접속 호스트를
|
||||
`127.0.0.1`로 바꾸면 세 건 모두 `TLS_PERMANENT`가 된다 (`EVD-332`).
|
||||
|
||||
**17/18은 확인, 1/18은 번복.** 이 비율 자체가 사이클 1 문서의 신뢰도에 대한 측정치다.
|
||||
|
||||
---
|
||||
|
||||
### 1.2 그 뒤에 이어진 전수 통독 — 23개 리프
|
||||
|
||||
18개 재검증과 별개로, 사이클 2 는 `ssotReview: FULL_READ_REQUIRED` 로 열려 있던 23개 리프를 닫았다.
|
||||
그 리프들의 사이클 1 SSOT 는 production 구현을 `STRUCTURAL_ONLY` 로 판정하고 파일 이름·LOC·build.gradle
|
||||
주석으로 서술한 상태였다.
|
||||
|
||||
**통독은 두 번에 걸쳐 이뤄졌고, 첫 번째는 통독이 아니었다.** 첫 판에서 23개 리프를 `FULL_READ_DONE` 으로
|
||||
표시하고 각 SSOT 에 "production N파일 축자 통독 완료" 를 적었으나, 리프마다 읽은 것은 일부였다. 그 상태로는
|
||||
Coverage ledger 가 사실이 아니므로 23개 리프를 다시 세고 파일 단위로 다시 읽었다.
|
||||
|
||||
| 대상 | 리프 | main 파일 | main 줄 | test 파일 | test 줄 |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| grpc 계열 | 18 | 260 | 18,726 | 55 | 9,383 |
|
||||
| messaging 계열 | 5 | 97 | 10,816 | 66 | 10,373 |
|
||||
| **합계** | **23** | **357** | **29,542** | **121** | **19,756** |
|
||||
|
||||
각 SSOT 가 주장을 거는 test 파일도 전부 읽었다. 통독 후 `STRUCTURAL_ONLY` 잔여는 0 이고,
|
||||
23개 SSOT 의 Coverage ledger 는 위 숫자로 다시 썼다.
|
||||
|
||||
**결과의 성격.** 통독이 만든 것은 대부분 새로운 사고가 아니라 **이미 알려진 패턴의 정확한 위치**다.
|
||||
사이클 1 의 가족 문서(19·20)는 두 가족을 각각 하나의 문서로 다루면서 "블록 전체가 미배선" 이라는 층위에서
|
||||
멈췄고, 통독은 그 블록 안에서 배선되더라도 성립하지 않을 것들을 찾았다.
|
||||
|
||||
**다만 P1 이 둘 나왔다.** 앞선 판은 "새 finding 에 P1 은 0" 이라고 적었고 그것은 부분 통독의 결과였다.
|
||||
둘 다 messaging 계열이고, 둘 다 "선언과 실제가 반대인데 관측은 정상" 이라는 §3.2 의 형태다.
|
||||
|
||||
1. **운영 프로파일에 TLS 와 브로커 인증을 요구해 놓고, 그 둘이 없는 생산자를 만든다.**
|
||||
`KafkaProfileValidator` 는 운영 프로파일이 전송 보안 없이 뜨는 것을 거부하고 그 거부를 테스트가 지킨다
|
||||
(`aProductionKafkaBrokerWithoutTransportSecurityFailsStartup`). 그런데 실제로 조립되는 `KafkaProducer`
|
||||
설정에는 `security.protocol` 이 없다 — Kafka 기본값 `PLAINTEXT` 다. 그 값을 만드는
|
||||
`KafkaSecurityConfigurer` 는 저장소 전역에서 production 호출자가 0 이다.
|
||||
(`messaging-spring-boot-starter` §17.1)
|
||||
2. **지원 문서가 `deduplicatedPublish` 를 지원으로 적고 코드는 거짓이며, 그 차이가 정확히 코드가 경고한 피해다.**
|
||||
(`messaging-kafka` §17.1)
|
||||
|
||||
이 둘이 P1 인 이유는 배선 여부와 무관하게 성립하기 때문이다. 나머지 23개 리프의 finding 대부분은
|
||||
"조립되면 성립하는 결함" 이지만, 이 둘은 messaging 계열이 실제로 배선되는 경로 위에 있다.
|
||||
|
||||
그 아래 층위에서 가장 무거운 예 셋:
|
||||
|
||||
1. **`JpaGrpcOperationLedger.claim` 의 insert-first 주장이 Spring Data 의 `save` 계약과 어긋난다.**
|
||||
엔티티의 식별자가 배정값이라 `save` 가 `merge` 로 가고, 파생 기본 키가 유니크 제약과 같은 행을 가리키므로
|
||||
두 번째 청구가 유니크 위반을 일으키지 않고 **커밋된 결과를 덮어쓴다.** 테스트 이중의 `save` 는 INSERT 를
|
||||
흉내 내 그 차이를 가린다. (`grpc-operation-ledger-jpa` §17.1)
|
||||
2. **`GrpcCredentialRotationManager.completeDrain()` 이 진행 중인 회전을 되돌린다.** 읽기와 쓰기 사이에
|
||||
회전이 일어나면 방금 교체된 자격증명이 되살아난다. (`grpc-policy` §17.2)
|
||||
3. **`GrpcPlatformStartupValidator` 가 시작 시 실행되지 않는다.** 그 검증기가 유일한 소비자인 설정 키 넷
|
||||
(`transport`·`tls-enabled`·`trust-all-certificates`·`operation-ledger-enabled`)이 아무것도 게이트하지 않는다.
|
||||
(`grpc-spring-boot-starter` §17.1)
|
||||
|
||||
셋 다 "조립되면 성립하는 결함" 이고, 블록 수준 서술로는 보이지 않는 층위다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 배포 지도 — 등록된 것과 배포되는 것의 거리
|
||||
|
||||
이 저장소에는 런타임 컴포지션이 둘 있다(`runtime_compositions`: `app-bootstrap`, `sample-portfolio`).
|
||||
62개 리프의 소속을 그대로 세면 다음과 같다.
|
||||
|
||||
| 소속 | 리프 수 |
|
||||
|---|---:|
|
||||
| `app-bootstrap`에 속함 | 33 |
|
||||
| `sample-portfolio`에만 속함 | 2 (`adapter-outbound-objectstorage`, `sample-portfolio`) |
|
||||
| **어느 컴포지션에도 속하지 않음** | **27** |
|
||||
|
||||
속하지 않는 27개의 내역:
|
||||
|
||||
- **grpc 블록 18개 전부** — `grpc-*` 9개와 `grpc-advanced-*` 9개. 이는 결함이 아니라 명시된 상태다.
|
||||
`src/grpc/CLAUDE.md:74-98`이 블록의 build-only 상태를 정확히 적고 있고, 사이클 2의 측정은 그 서술의
|
||||
확인이다 (`EVD-325`).
|
||||
- **messaging 7개** — `schema-avro`, `schema-protobuf`, `kafka-share-experimental`,
|
||||
`pulsar-experimental`, `nats-experimental`, `spring-cloud-stream-bridge`, `testkit`.
|
||||
실험 어댑터와 선택적 스키마·테스트 지원이라는 성격상 예상되는 목록이다.
|
||||
- **`adapter-inbound-grpc`와 `adapter-inbound-websocket`** — 이 둘은 위 두 범주 어디에도 속하지 않는
|
||||
일반 인바운드 어댑터인데 어떤 배포에도 들어가지 않는다.
|
||||
|
||||
여기서 리프 경계를 넘어야만 보이는 사실이 하나 있다. `adapter-inbound-websocket` 문서는 이 리프의
|
||||
내부 구조를 충실히 기술하지만, "이 리프가 어떤 배포에도 없다"는 것은 리프 안에서는 보이지 않는다 —
|
||||
`modules.json`의 다른 항목과 대조해야 나온다. 같은 형태로 `adapter-outbound-objectstorage`는
|
||||
**샘플에만** 있다. 출하 애플리케이션에는 오브젝트 스토리지 어댑터가 없다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 저장소 전체를 관통하는 패턴
|
||||
|
||||
422건을 finding 제목 텍스트에 대해 기계 분류했다. 제목만 읽는 분류이므로 아래 수치는 **하한**이고
|
||||
census가 아니다 — 268건은 제목이 너무 짧아 어느 유형에도 걸리지 않았다.
|
||||
|
||||
| 유형 | 건수 | P1 | P2 | P3 | 나타난 리프 |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| A. 만들어졌지만 조립되지 않음 | 57 | 8 | 27 | 22 | **23** |
|
||||
| F. 문서·주석·이름이 코드와 다름 | 57 | 1 | 20 | 36 | 18 |
|
||||
| E. 동시성·경합·순서 | 26 | 5 | 8 | 13 | 12 |
|
||||
| B. 선언은 통과하는데 강제하는 주체가 없음 | 14 | 4 | 6 | 4 | 9 |
|
||||
| D. 같은 문제에 메커니즘이 둘 이상 | 12 | 0 | 2 | 10 | 9 |
|
||||
| C. 테스트가 픽스처를 검증함 / 검증 공백 | 11 | 4 | 2 | 5 | 6 |
|
||||
|
||||
### 3.1 A — 만들어졌지만 조립되지 않는다 (23개 리프)
|
||||
|
||||
이 저장소에서 압도적으로 반복되는 형태다. 세 층위로 나타난다.
|
||||
|
||||
**층위 1 — 컴포지션 루트가 패키지를 제외한다.** `adapter-inbound-web`의 P1 여섯 건은 전부 하나의
|
||||
원인으로 수렴한다. 컴포지션 루트가 `mvc.error`·`webflux.error`·`mvc.budget`·`mvc.operation`·
|
||||
`webflux.operation` 다섯 패키지를 컴포넌트 스캔에서 제외하고, 그 결과 RFC 9457 계약 23개 파일,
|
||||
용량 보호 계층 41개, 멱등 실행 계층 38개가 출하 애플리케이션에 등록되지 않는다. 그 문서 자신이
|
||||
여섯 번째 finding에서 단일 원인을 지목한다 — "다섯 레인·세 런타임 패리티가 검증하는 것은 픽스처의
|
||||
조립이고, '플랫폼이 능력을 설치하는가'를 묻는 레인이 없다."
|
||||
|
||||
**층위 2 — 클래스는 있는데 생성자가 없다.** `messaging-inbox-jdbc-postgresql`의 bounded purge는
|
||||
구현돼 있고 호출되지 않는다. `messaging-outbox-jdbc-postgresql`은 그 반대 방향으로 같은 형태다 —
|
||||
무제한 DELETE가 호출되고, 그것을 막는 bounded 오버로드가 호출되지 않는다.
|
||||
`messaging-spring-boot-starter`의 종료 수명주기는 아무도 증가시키지 않는 카운터가 0이 되기를 기다린다.
|
||||
`adapter-outbound-httpclient`의 `POOL_ROUTE_EXCEEDS_TOTAL` 위반 코드는 생성자에 가려 발화할 수 없다.
|
||||
|
||||
**층위 3 — 블록 전체.** grpc 18개 리프. 이 경우만은 저장소가 그 상태를 문서로 인정하고 있다.
|
||||
|
||||
이 세 층위의 차이가 중요하다. 층위 3은 **선언된 미완성**이고, 층위 1·2는 **선언되지 않은 미완성**이다.
|
||||
같은 저장소가 전자를 다루는 좋은 선례를 갖고 있다 —
|
||||
`MessagingProviderSelection.BROKERS_WITHOUT_A_TRANSPORT`는 rabbit의 미완성에 이름을 붙이고 기동에서
|
||||
거절하며 "An entry leaves this map on the day its transport does exist"라고 적는다. 층위 1·2에는
|
||||
그런 이름이 없다.
|
||||
|
||||
### 3.2 B — 검증기는 통과시키고, 그 값을 읽는 코드는 없다 (9개 리프)
|
||||
|
||||
A의 특수형이지만 결과가 다르다. A는 기능이 없는 것이고, B는 **없는 기능이 있다고 보고되는 것**이다.
|
||||
|
||||
가장 선명한 사례는 `adapter-outbound-persistence-mongo`다. `MongoClientSettingsFactory`가 저장소
|
||||
전체에서 호출되지 않아 프로파일의 `tlsRequired`·타임아웃·풀 상한·Stable API·UUID 표현이 드라이버에
|
||||
도달하지 않는다. 검증기는 "TLS 필수" 선언을 통과시키고, 연결은 평문일 수 있다. 선언과 실제가
|
||||
반대인데 관측은 정상이다.
|
||||
|
||||
같은 형태가 `messaging-pulsar-experimental`의 `claimCheckThresholdBytes`에 있다 — 필드는 있고
|
||||
그것을 읽는 액터가 없다. `adapter-outbound-persistence-jpa`의 바이트 쿼터도 같다 — persistent
|
||||
`reserved + committed` 값을 실제 admission에서 읽거나 상한과 비교하는 경로가 없다.
|
||||
|
||||
**사이클 2 가 더한 것 — 능력 선언이 프로파일에서 파생되지 않는다 (3개 어댑터).**
|
||||
|
||||
세 실험/출하 브로커 어댑터가 모두 `MessagingCapabilities` 를 **상수** 로 둔다. 그리고 그 상수가 답하는 것과
|
||||
그 능력이 실제로 성립하는 조건이 갈린다.
|
||||
|
||||
| 어댑터 | 상수가 답하는 것 | 실제 조건 |
|
||||
|---|---|---|
|
||||
| `messaging-nats-experimental` | `deduplicatedPublish = true` | 프로파일에 중복 제거 창이 있을 때만 `Nats-Msg-Id` 를 보낸다 |
|
||||
| `messaging-kafka` | `brokerTransaction = true` | 트랜잭션 식별자 접두·멱등 생산자·`acks=all`·수동 커밋이 모두 필요하고, 그것을 검사하는 검증기는 주입되지 않는다 |
|
||||
| `messaging-pulsar-experimental` | 전송과 검증기가 `orderedStream` 에 **서로 다른** 값 | Key_Shared 의 순서 단위는 키다 |
|
||||
|
||||
첫째가 특히 무겁다. `deduplicatedPublish` 는 능력 열둘 중 부재가 예외를 만드는 유일한 플래그이므로
|
||||
(`DefaultMessagePublisher:250`), 창 없는 목적지가 그 가드를 통과한다.
|
||||
|
||||
이 셋은 B 의 거울상이다. B 는 선언을 통과시키고 읽는 코드가 없는 것이고, 이것은 **읽히는 값이 조건과
|
||||
무관하게 참** 인 것이다.
|
||||
|
||||
### 3.3 C — 레인이 검증하는 것이 픽스처의 조립일 때 (6개 리프)
|
||||
|
||||
`adapter-inbound-web`의 다섯 레인, `grpc` 경계 규칙이 실제 소스를 보지 않는 것(`EVD-328`),
|
||||
`messaging-testkit`의 인증 매니페스트 드리프트(`EVD-300`)가 같은 계열이다.
|
||||
|
||||
여기에 사이클 2가 사례 하나를 보탠다 — `app-bootstrap`의 `everyLaneMatchesItsContract`는
|
||||
`docker compose` 부재는 skip으로 막고 `jq` 부재는 실패로 낸다. 스크립트는 전제 결손을 `exit 78`
|
||||
(sysexits.h의 `EX_CONFIG`)로, 계약 위반을 `exit 1`로 **구분해서** 알리는데 테스트가 그 구분을 버린다.
|
||||
결과적으로 아무것도 검사되지 않은 상태가 "레인이 계약과 다르다"로 보고된다 (`EVD-334`).
|
||||
|
||||
### 3.4 D — 같은 문제에 메커니즘이 둘 (9개 리프)
|
||||
|
||||
`messaging-admin-runtime`의 두 토폴로지 스택(`EVD-307`), `messaging-kafka`의 세 층위 재시도 중
|
||||
하나만 도는 것, grpc의 두 규칙 엔진 중 하나만 급여되는 것(`EVD-329`),
|
||||
`adapter-outbound-httpclient`의 재생 가능성 판정이 호출마다 반사로 재계산되는 것.
|
||||
|
||||
이 유형이 P3에 몰려 있는 것(12건 중 10건)은 우연이 아니다. 둘 중 하나는 대개 돌고 있어서
|
||||
증상이 없다. 비용은 **다음에 고치는 사람이 어느 쪽이 정본인지 모른다**는 데서 나온다.
|
||||
|
||||
### 3.5 E — 동시성·경합 (12개 리프)
|
||||
|
||||
P1 다섯 건이 여기 있다. `adapter-outbound-persistence-jpa`의 outbox stale worker가 owner fencing
|
||||
없이 newer/terminal 상태를 덮어쓰는 것, provider call recorder가 lease-unaware `save()`를 써서
|
||||
만료된 holder의 stale projection이 새 holder를 덮는 것, `adapter-outbound-persistence-mongo`의
|
||||
change stream high-water mark가 "본 위치"여서 failover 중 이벤트가 조용히 영구 소실되는 것,
|
||||
`messaging-admin-runtime`의 재개된 리드라이브가 옮기지 못한 메시지를 영구히 건너뛰는 것.
|
||||
|
||||
이 넷의 공통 형태는 **실패가 상태로 남지 않는다**는 것이다. 넷 다 로그도 상태 전이도 남기지 않고,
|
||||
`RUNNING` 또는 성공으로 보이는 채로 데이터가 사라지거나 덮인다.
|
||||
|
||||
**사이클 2 가 더한 것 — 원자 타입을 쓰면서 비교 후 교체를 하지 않는다 (5곳).**
|
||||
|
||||
전수 통독이 찾은 가장 일관된 형태다. 다섯 곳이 `AtomicReference`·`AtomicInteger` 를 선택해 놓고
|
||||
읽고-판단하고-쓰는 세 단계를 원자적으로 묶지 않는다.
|
||||
|
||||
| 위치 | 형태 | 결과 |
|
||||
|---|---|---|
|
||||
| `GrpcCredentialRotationManager.rotate`·`completeDrain` | `get()` 후 조건 없는 `set()` | 회전이 되돌아가 교체된 자격증명이 되살아난다 |
|
||||
| `GrpcChannelRuntimeRegistry.rotate` | 〃 (같은 파일의 `install` 은 CAS 를 쓴다) | 덮인 대체본이 배수도 회수도 되지 않는다 |
|
||||
| `GrpcChannelRuntime.finishUnaryCall`·`closeStream` | `get() > 0` 후 별도 감소 | 음수가 되면 `quiescent()` 가 영원히 거짓 → 세대가 회수 불가 |
|
||||
| `GrpcAdmissionController.tryAdmit`·`release` | 〃 | 경계 초과, 그리고 경계가 영구히 느슨해짐 |
|
||||
| `GrpcStreamAdmission.tryAdmit`·`release` | 〃 + caller 별 맵이 줄지 않음 | 재접속 폭풍에서 가장 많이 샌다 |
|
||||
|
||||
같은 저장소 안에 정본이 둘 있다 — `GrpcRetryBudget.tryConsume` 과 `GrpcHedgingBudget.tryConsume` 이
|
||||
정확한 비교 후 교체 루프다. 그리고 `GrpcDemandController` 는 같은 형태를 `synchronized` 로 닫는다.
|
||||
즉 이 저장소는 올바른 형태를 알고 있고, 다섯 곳에서만 쓰지 않았다.
|
||||
|
||||
**사이클 2 가 더한 것 — 선언되고 주입되지 않는 검증기 (4곳).**
|
||||
|
||||
`StartupProfileValidation` 의 javadoc 이 이 형태를 이미 이름 붙였다 — "the context published a validator
|
||||
per broker and validated nothing." 그 수정이 messaging 에 적용됐는데, 같은 형태가 네 곳에 남아 있다.
|
||||
|
||||
| 검증기 | 상태 |
|
||||
|---|---|
|
||||
| `GrpcPlatformStartupValidator` | 호출자 0 (자기 테스트 제외) |
|
||||
| `KafkaTransactionProfileValidator` | 빈으로 발행되고 `StartupProfileValidation` 에 감싸이지 않음 |
|
||||
| `GrpcApplicationBoundaryRules` | 저장소 소스에 적용하는 코드 없음 |
|
||||
| `GrpcRawApiImportRule` | 〃 (테스트가 인라인 문자열만 판정) |
|
||||
|
||||
**재통독이 다섯 곳을 더 찾았다.** 그리고 그중 셋은 앞의 넷보다 무겁다 — **문서가 그 검증기를 "빌드를
|
||||
실패시키는 것" 이라고 단언하기 때문이다.** 아무도 부르지 않는 검증기는 공백이고, 부른다고 적힌 채
|
||||
아무도 부르지 않는 검증기는 오해다.
|
||||
|
||||
| 검증기 / 게이트 | 상태 | 그렇게 적은 곳 |
|
||||
|---|---|---|
|
||||
| `GrpcProtoContractValidator` | 코드 호출자 0. `*.gradle`·`*.kts`·`*.yml` 어디에도 없음 | `buf.yaml:3-5` 와 `GrpcBufPolicy:8-10` 이 각각 "이것이 이 저장소의 빌드를 실패시킨다" 고 적는다 |
|
||||
| `GrpcBufPolicy` 의 네 수명주기 태스크 | `bufFormatCheck`·`bufLint`·`bufBuild`·`bufBreaking` 이 어떤 빌드 파일에도 없음 | javadoc 이 "a missing stage is a test failure rather than a stage nobody noticed was gone" 라고 적는다. 테스트는 목록을 리터럴·자기 자신과 비교한다 |
|
||||
| `GrpcAdvancedModuleGuard.requireStableStarterIsClean` | 호출자 0 | javadoc 이 "a runtime that was assembled some other way — a fat jar, a shaded artifact, a test harness — is checked too" 라고 적는다 |
|
||||
| `NatsJetStreamProfileValidator` | 코드 호출자 0 · 테스트 0. 흔적은 javadoc `{@link}` 한 줄 | `NatsJetStreamTransport:35` 가 "refuses the combination at startup" 이라고 적는다 |
|
||||
| `PulsarProfileValidator` | 저장소 전체에서 자기 선언 한 줄 말고 아무 데도 없음 | 그 리프 SSOT §4 가 "검증기가 합의를 요구한다" 로 서술했다(이번에 정정) |
|
||||
|
||||
`GrpcProtoContractValidator` 와 `GrpcBufPolicy` 는 서로를 가리킨다 — `buf.yaml` 은 CLI 가 없으니 자바
|
||||
검증기가 게이트라고 하고, `GrpcBufPolicy` 는 태스크 이름이 CI 의 계약이고 자바 검증기가 실제 게이트라고
|
||||
한다. 두 쪽 다 상대가 게이트라고 말하고, 어느 쪽도 실행되지 않는다.
|
||||
|
||||
**형태의 이름.** 앞의 넷은 "만들어졌지만 조립되지 않음"(§3.1 A)이고, 이 다섯은 **"조립되었다고 적힌 채
|
||||
조립되지 않음"** 이다. A 와 F(문서가 코드보다 앞섬)가 같은 지점에서 겹치는 자리이고, 이 저장소의 주석
|
||||
밀도가 높기 때문에 겹칠 때 특히 비싸다 — 읽는 사람이 게이트의 존재를 근거 있게 믿게 된다.
|
||||
|
||||
### 3.8 H — 선언만 있고 코드가 닿지 않는 project 의존 (재통독 신설, 6곳)
|
||||
|
||||
리프 SSOT 는 자기 build.gradle 을 읽지만, "이 의존이 실제로 쓰이는가" 는 그 리프의 import 를 전수로 봐야
|
||||
나온다. 재통독이 그것을 리프마다 확인했고 여섯 곳이 나왔다.
|
||||
|
||||
| 리프 | 선언 | 실제 import |
|
||||
|---|---|---|
|
||||
| `grpc-advanced-edition` | `grpc-core-api` · `grpc-proto-contract` · `grpc-advanced-bootstrap` | **0** — `dev.caskeleton` import 가 한 줄도 없다 |
|
||||
| `grpc-advanced-diagnostics` | 위 셋 중 `grpc-client` 포함 셋 | bootstrap 만 4줄. `grpc-core-api`·`grpc-client` 0 |
|
||||
| `grpc-spring-boot-starter` | `grpc-proto-contract` · `grpc-codegen` · `grpc-operation-ledger-jpa` (implementation) | 셋 다 0 |
|
||||
| `grpc-advanced-streaming` · `-compat` | `grpc-advanced-bootstrap` | 둘 다 0 |
|
||||
| `grpc-testkit` · `grpc-spring-boot-starter` | `grpc-observability` (api) | 둘 다 0 |
|
||||
|
||||
`api` 로 선언된 것은 그 모듈을 쓰는 쪽까지 전파된다. `grpc-observability` 의 경우 Micrometer 가 두 모듈을
|
||||
거쳐 전파되는데, 그 두 모듈은 관측 타입을 하나도 쓰지 않는다.
|
||||
|
||||
**이 유형이 A 와 다른 점.** A 는 코드가 있고 부르는 곳이 없는 것이고, 이것은 **의존 그래프가 코드보다 넓은
|
||||
것**이다. 결과는 반대 방향으로 나타난다 — A 는 기능이 없는 것으로, 이것은 경계가 실제보다 느슨해 보이는
|
||||
것으로. `grpc-spring-boot-starter` 는 이 저장소가 "구성 경계" 라고 이름 붙인 리프이므로, 그 리프의 의존이
|
||||
실제 조립에 필요한 것보다 넓다는 사실은 그 이름이 주장하는 바에 직접 걸린다.
|
||||
|
||||
의도를 읽을 수 있는 경우도 있다 — `grpc-advanced-edition` 의 `grpc-proto-contract` 의존은 그 리프의
|
||||
`compatibility.proto` 가 저쪽 스키마 규칙의 관할이라는 선언으로 읽힌다. 다만 그 관할은 코드로 연결되어
|
||||
있지 않고, `grpc-proto-contract` 의 커밋 스키마 테스트가 파일 목록을 하드코딩해 이 파일을 판정하지 않는다.
|
||||
즉 의존 선언이 표현하려던 관계가 실제로는 어느 쪽에도 없다.
|
||||
|
||||
### 3.6 F — 문서가 코드보다 앞서 있다 (18개 리프, 57건)
|
||||
|
||||
건수로는 A와 동률 1위인데 P1이 하나뿐이다. 대부분 javadoc·README·주석이 이제는 사실이 아닌 것을
|
||||
말하는 형태다. `adapter-outbound-httpclient`의 `BoundedDataBufferFlux`가 javadoc이 처리한다고 적은
|
||||
두 경로가 no-op인 것, `adapter-outbound-fileserver`의 README 주장이 여덟 개 port 구현 앞에서
|
||||
성립하지 않는 것(P2)이 대표적이다.
|
||||
|
||||
이 저장소의 주석 밀도는 이례적으로 높고 — `adapter-outbound-httpclient`의 `build.gradle`은 이 저장소에서
|
||||
가장 긴 근거 주석을 갖는다 — 그 밀도가 자산인 동시에 부채라는 것이 이 유형의 내용이다. 사고를 인용하는
|
||||
주석은 그 사고를 다시 겪지 않게 하지만, 코드가 바뀔 때 함께 바뀌지 않으면 **틀린 근거를 권위 있게**
|
||||
전달한다.
|
||||
|
||||
---
|
||||
|
||||
### 3.7 G — 전송 계열 가정 (사이클 2 신설)
|
||||
|
||||
`EVD-332` 가 사이클 1 의 P1 을 철회시킨 원인은 픽스처의 듀얼스택 호스트명이었다. 즉 **IPv4 만 가정한 코드가
|
||||
IPv6 가 있는 환경에서 다르게 동작한다** 는 형태다. 통독이 같은 형태를 하나 더 찾았다.
|
||||
|
||||
`GrpcDiagnosticsRedactor.maskAddress` 는 IPv4 정규식 하나만 갖고, 맞지 않는 입력을 **그대로 돌려준다.**
|
||||
그리고 스냅숏 생성자의 검사가 "마스킹 결과가 입력과 같으면 이미 마스킹된 것" 이므로, IPv6 주소·호스트 이름·
|
||||
유닉스 소켓 경로가 전부 검사를 통과한다. 이 플랫폼이 겨냥하는 배포 형태가 쿠버네티스이고 헤드리스 레코드의
|
||||
엔드포인트가 파드 DNS 이름이라는 점에서 도달 가능한 형태다.
|
||||
|
||||
두 사례의 공통점은 **주소 표현의 다양성** 이 아니라 **판정의 방향** 이다. 둘 다 "모르는 형태" 를 안전한 쪽이
|
||||
아니라 통과 쪽으로 접었다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 리프 경계를 넘을 때만 보이는 것
|
||||
|
||||
리프 SSOT가 원칙적으로 볼 수 없는 사실을 여기 모은다.
|
||||
|
||||
1. **`adapter-inbound-grpc`·`adapter-inbound-websocket`이 어떤 배포에도 없다.** §2.
|
||||
2. **`adapter-outbound-objectstorage`가 샘플에만 있다.** 출하 애플리케이션에는 없다. §2.
|
||||
3. **`messaging-admin-api`의 토폴로지 BLOCKING 보장을 배선하려면 인스펙터가 필요한데,
|
||||
`messaging-kafka`에는 줄 것이 없다.** 두 리프의 문서가 각자 자기 쪽 절반만 볼 수 있다 —
|
||||
admin-api는 "`@ConditionalOnBean(BrokerTopologyInspector)`가 참이 된 적이 없다"를 보고,
|
||||
kafka는 "`KafkaTopologyInspector` 구현이 0"을 본다. 둘을 겹쳐야 **같은 하나의 미배선**이 된다.
|
||||
4. **`adapter-inbound-web`의 P1 여섯 건은 web 리프가 아니라 `app-bootstrap`이 원인이다.**
|
||||
web 문서가 그 사실을 스스로 지목하지만, 고칠 파일은 다른 리프에 있다.
|
||||
5. **grpc 블록의 Stable/Advanced 경계는 실제로 강제된다.** 사이클 2에서 한 번 반대로 판단했다가
|
||||
`grpc-spring-boot-starter/build.gradle:4-7`의 근거 주석을 따라가 세 겹의 강제
|
||||
(레지스트리 `allowed_dependencies` · `verifyCleanArchitectureDependencies` ·
|
||||
`GrpcPlatformStartupValidatorTest:257`이 실제 `build.gradle`을 읽는 것)를 확인하고 철회했다
|
||||
(`EVD-326`). 남은 것은 카탈로그의 이름 목록이 `modules.json`과 대조되지 않는다는 P3뿐이다.
|
||||
|
||||
---
|
||||
|
||||
6. **같은 자료구조 오용이 두 리프에 있다.** `GrpcCredentialRotationManager` 와
|
||||
`GrpcChannelRuntimeRegistry` 가 각각 `AtomicReference` 를 조건 없는 `set` 으로 쓴다. 두 리프의 문서는
|
||||
각자 자기 쪽만 볼 수 있고, 겹쳐야 "이 가족이 회전을 다루는 방식" 이라는 하나의 사실이 된다. §3.5.
|
||||
7. **Kafka 트랜잭션 검증의 절반이 다른 리프에 있다.** 검증기는 `messaging-kafka` 가 소유하고, 그것을
|
||||
시작 시 부르는 배선은 `messaging-spring-boot-starter` 가 소유한다. 후자에 감싸는 블록이 없어서 전자가
|
||||
돌지 않는다. 어느 쪽 문서도 혼자서는 "이 검증이 실행되지 않는다" 를 말할 수 없다.
|
||||
8. **정책 목록의 가장 강한 성질이 다른 리프의 미완성에 걸려 있다.** `GrpcMethodPolicyCatalog` 의
|
||||
서술자 대조는 이름 변경을 잡는 장치인데, 서술자를 만드는 `grpc-codegen` 이 protoc 을 돌리지 않으므로
|
||||
이 저장소에서는 그 대조를 켤 수 없다. `withDescriptorMethods` 의 production 호출자는 0 이다.
|
||||
10. **같은 문제의 올바른 판본과 틀린 판본이 두 리프에 나란히 있다 — 결정을 그 결정이 판정한 대상에 묶는 것.**
|
||||
`grpc-codegen` 의 `GrpcSchemaArtifactPublisher.publish(candidate, decision)` 는 `decision.allowed()` 만
|
||||
보고 기록한다. `PublishDecision` 은 자기가 무엇을 판정했는지 들고 있지 않으므로, A 를 평가한 결정으로
|
||||
B 를 발행할 수 있고 그러면 소비자 게이트와 버전 불변성을 둘 다 우회한다(그쪽 §17.4).
|
||||
`grpc-advanced-bootstrap` 의 `GrpcAdvancedSupportMatrix.apply(decision)` 는 정반대다 — 결정의 `from` 이
|
||||
현재 등급과 다르면 던지고, 그 이유를 "두 승격이 경합했거나 하나가 재생된 경우" 라고 적는다.
|
||||
같은 저장소가 같은 형태를 한 번은 맞게, 한 번은 틀리게 썼다. §3.5 의 check-then-act 계열과 같은 뿌리이나
|
||||
여기서는 경합이 아니라 **인자 짝 맞추기**가 깨진 자리다.
|
||||
|
||||
11. **"실환경 증거" 의 정의와 그 요구가 다른 리프에 있고 서로를 부르지 않는다.**
|
||||
`grpc-advanced-diagnostics` 의 `GrpcAdvancedInfrastructureTestkit` 이 능력별로 무엇이 실환경인지
|
||||
정의한다(gRPC-Web 프록시 · 서블릿 컨테이너 · 멈출 수 있는 xDS 통제 평면 · 코틀린 툴체인).
|
||||
`grpc-advanced-bootstrap` 의 `GrpcAdvancedPromotionEvidence` 는 그것을 `boolean realEnvironmentTest`
|
||||
하나로 받는다. 두 쪽이 만나지 않으므로 `complete(XDS, 7일)` 이 통제 평면 없이도 참을 넣는다 —
|
||||
테스트킷 javadoc 이 경계한 "a suite that runs without the infrastructure passes and establishes
|
||||
nothing" 을 승격 게이트가 그대로 통과시킨다.
|
||||
|
||||
12. **승격 기준이 같은 능력에 대해 두 게이트에서 다르다.** `EDITION_2024` 는 `GrpcAdvancedPromotionGate`
|
||||
에서 증거 일곱 항목 + 7일 담금을 요구받고, `GrpcEdition2024Gate` 에서 호환성 보고서 + 소비자 이관 +
|
||||
ADR 을 요구받는다. 어느 쪽도 상대를 부르지 않고 관계가 문서에도 없다. 그리고 전자에는 별도 결함이 있다 —
|
||||
30일 담금 갈래가 열거형에 없는 등급(`STABLE_DEFAULT`)을 위해 쓰여, `ADVANCED_STABLE` 이 아닌 목표 전부를
|
||||
삼킨다. 그 결과 `WATCH → EXPERIMENTAL`(WATCH 가 밟도록 강제된 유일한 첫 걸음)이 30일을 요구하고
|
||||
`EXPERIMENTAL → ADVANCED_STABLE` 은 7일을 요구한다 — 중간 등급이 상위 등급보다 어렵다.
|
||||
그 갈래를 실행하는 테스트가 `ADVANCED_STABLE → DISABLED`(철회)를 골라 놓고 이름을
|
||||
"becoming a Stable default" 라고 붙인 것이 그 뒤틀림의 흔적이다. (`grpc-advanced-bootstrap` §17.4)
|
||||
|
||||
9. **Stable 모듈 목록과 레지스트리를 붙드는 장치가 없다.** `GrpcStableModuleCatalog` 의 javadoc 은
|
||||
`GrpcStableModuleCatalogTest` 가 둘을 함께 붙든다고 적지만, 그 테스트는 `modules.json` 을 읽지 않고
|
||||
목록을 리터럴과 대조한다. 두 집합은 오늘 일치한다(12 + 6 = 레지스트리의 grpc 계열 18). 어긋난 것은
|
||||
그 일치를 무엇이 지키는가다. messaging 가족이 같은 형태를 이미 기록했다 — "세는 순간 다시 drift 한다."
|
||||
|
||||
---
|
||||
|
||||
## 5. 측정 방법에 대해 이 사이클이 배운 것
|
||||
|
||||
사이클 2가 만든 판정 번복 한 건과 자기 교정 세 건은 모두 같은 형태의 실수에서 나왔다.
|
||||
**코드를 읽고 런타임의 모양을 추론한 뒤, 그 추론을 측정으로 확인하지 않은 것.**
|
||||
|
||||
- `EVD-332` — 분기 순서를 읽고 예외 사슬의 모양을 단정했다. 사슬을 출력하니 달랐다.
|
||||
- `EVD-326` — 레지스트리에 검사가 없다고 단정했다. `build.gradle`의 주석이 가리키는 세 곳을 따라가니 있었다.
|
||||
- `PulsarPreSendRejection` — 전역 승인 게이트가 임계값을 읽을 것이라고 가정했다. `PayloadLimitGuard`는 그 필드를 읽지 않았다.
|
||||
- outbox 무제한 DELETE의 서술 — "프로덕션 기본 경로"라고 적었다. 자동설정은 스케줄러를 등록하지 않는다.
|
||||
|
||||
네 건 모두 **하나의 추가 측정이면 갈렸다**. 이 저장소의 코드는 자기 근거를 주석으로 남기는 밀도가 높아서
|
||||
읽는 것만으로 확신이 생기기 쉽고, 바로 그 점이 함정이다. 사이클 3에 남기는 규칙은 하나다 —
|
||||
**런타임의 모양에 대한 주장은 런타임에서 확인한다.** jshell로 사슬을 출력하는 데 든 비용은 몇 분이었고,
|
||||
그 몇 분이 P1 하나를 지웠다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 확인하지 못한 것
|
||||
|
||||
- **다른 두 전송 분류기.** `ReactorFailureClassifier`·`JdkFailureClassifier`는 개별 실행으로 확인하지
|
||||
않았다. `EVD-332`가 반증한 것은 "Apache가 TLS 실패를 연결 예외로 감싼다"는 전제이므로 그 전제에
|
||||
기대던 열린 항목은 소멸하지만, 두 분류기 자체를 측정한 것은 아니다.
|
||||
- **`jq`가 있는 기계에서의 `verify-compose-profile-contracts.sh`.** 파이썬 이식본으로 15개 레인이
|
||||
계약과 일치함을 확인했으나, 이는 테스트 자신이 경계한 "두 번째 의견"이다. 원본 스크립트 실행이 정본이다.
|
||||
- **422건 중 268건의 기계 분류.** 제목이 짧아 유형 분류가 되지 않았다. §3의 수치는 하한이다.
|
||||
그리고 그 분류는 **재통독 이전의 422건에 대해 돌린 것**이다. 23개 리프가 60→100 으로 늘어난 뒤 다시
|
||||
돌리지 않았으므로, §3 의 유형별 건수는 새로 추가된 40건을 반영하지 않는다. §3.5 의 검증기 표와 §3.8 은
|
||||
기계 분류가 아니라 재통독에서 직접 확인해 손으로 적은 것이다.
|
||||
- **전체 462건의 직접 재측정.** §0 이 밝힌 대로 462는 델타 조정값이고 61개 문서를 다시 센 값이 아니다.
|
||||
- **`sample-portfolio`.** 레지스트리에서 EXCLUDED이며 이 사이클의 분석 대상이 아니다.
|
||||
- **런타임 컴포지션의 실제 기동.** compose 계약은 정적으로만 검증했다. 아무 스택도 기동하지 않았다.
|
||||
- **사이클 2 통독이 만든 92건의 실행 확인.** 전부 코드 통독과 정적 대조로 판정했다. 두 가족 모두 배선 경로가
|
||||
없거나(grpc 18개 리프) 선택할 수 없어서(rabbit), 실행으로 재현할 대상이 애초에 없다. 예외는
|
||||
`grpc-testkit` 의 세 레인으로, 이번에 직접 돌려 통과를 확인했다(계약 7 · Netty 9 · 고장 9).
|
||||
- **재통독의 도달성 판정 방법.** 리프마다 `grep` 으로 타입 이름·패키지 이름을 훑어 리프 밖 참조를 셌다.
|
||||
리플렉션·서비스 로더·문자열 기반 조립으로 닿는 경로가 있다면 이 방법으로는 잡히지 않는다. 이 저장소가
|
||||
그런 조립을 쓰는 곳은 발견하지 못했으나, 찾아본 것이 아니라 마주치지 않은 것이다.
|
||||
- **23개 리프의 테스트 실행.** 재통독은 테스트 본문을 전부 읽었지만 이번 판에서 다시 돌리지는 않았다.
|
||||
"이 단언은 항상 통과한다" 류의 판정(`messaging-nats-experimental` §17.4 등)은 단언 의미론으로 내린 것이다.
|
||||
예외는 `grpc-testkit` 의 세 레인으로, 앞선 판에서 직접 돌려 통과를 확인했다.
|
||||
- **`@ConditionalOnBean` 사슬의 실제 평가.** `MessagingReliabilityAutoConfiguration` 이 같은 클래스 안에서
|
||||
방금 선언한 빈을 조건으로 삼는다. 지금은 그 앞 조건이 만족되지 않아 셋 다 만들어지지 않으므로 결과가
|
||||
드러나지 않는다. 스프링의 문서화된 제약으로 판정했고 컨텍스트로 재현하지 않았다.
|
||||
|
||||
---
|
||||
|
||||
|
||||
사이클 1 초안이 열린 질문 다섯 개를 번호로 관리했다. 그 제목이 `candidate-ledger.json`에
|
||||
보존되어 있어 아래에 그대로 되살린다 — 초안 본문은 복구되지 않았으므로, 각 항목의 내용은
|
||||
사이클 2가 실제로 확인한 것과 확인하지 못한 것으로 다시 썼다.
|
||||
|
||||
### 남은 질문 1 — 컨테이너·브로커·DB가 필요한 레인의 실제 결과
|
||||
|
||||
사이클 2는 Docker 가용을 확인하고(client 29.1.3 / server 29.6.1) Testcontainers 레인을 실제로 돌렸다 —
|
||||
persistence-jpa 477, persistence-mongo 72, cache-redis 435 테스트가 전부 통과했고, kafka 인증 레인과
|
||||
실 브로커 왕복, Postgres IT, grpc-testkit 엄격 레인 넷도 실행했다. 그러나 **전부는 아니다.**
|
||||
`jpaPlatformFailureTest`는 이 리비전에서 실행하지 않았고(§6의 커밋 모호성 항목), mongo 쪽 컨테이너 레인 중
|
||||
릴리스를 막지 않는 것들도 실행하지 않았다. 어느 레인이 실행됐고 어느 레인이 아닌지는 `EVD-334`가 목록으로 갖는다.
|
||||
|
||||
### 남은 질문 2 — sample-portfolio 내부
|
||||
|
||||
레지스트리에서 `EXCLUDED`이며 사용자 지시에 따라 이 사이클의 분석 대상이 아니다. 이 리프에만 속하는
|
||||
`adapter-outbound-objectstorage`가 출하 애플리케이션에 없다는 사실(§2)은 레지스트리 대조로 확인했지만,
|
||||
샘플 내부의 도메인 모델과 그 조립은 읽지 않았다.
|
||||
|
||||
### 남은 질문 3 — 런타임 관측
|
||||
|
||||
compose 계약은 정적으로만 검증했다. 어떤 스택도 기동하지 않았고, 애플리케이션을 부팅해 액추에이터나
|
||||
조건 평가 리포트를 읽지도 않았다. 부팅 한 번이면 확증되는 정적 추론이 최소 두 건 남아 있다 —
|
||||
messaging 관측 시리즈 부재와 `@ConditionalOnBean` 사슬의 실제 평가 결과다.
|
||||
|
||||
### 남은 질문 4 — `@ConditionalOnBean` 실제 평가 순서
|
||||
|
||||
`@ConditionalOnBean`이 클래스 파싱 시점에 평가되어 빈이 사라지는 계열의 판정은 코드와 javadoc의 사후
|
||||
기록에 근거한다. 이 리비전에서 `ConditionEvaluationReport`를 읽어 실제 평가 순서와 결과를 확인하지 않았다.
|
||||
`debug=true`로 부팅 한 번이면 확인된다.
|
||||
|
||||
### 남은 질문 5 — 성능·용량 주장
|
||||
|
||||
이 사이클은 성능을 측정하지 않았다. 문서에 남은 성능·용량 관련 서술은 전부 코드가 선언한 상한과 그 강제
|
||||
여부에 대한 것이며, 실제 처리량·지연·자원 사용에 대한 주장은 하지 않는다. httpclient의 성능 레인과 jmh
|
||||
벤치마크는 기계 의존적이라는 이유로 `check`에서 빠져 있고, 이 사이클에서도 돌리지 않았다.
|
||||
|
||||
## 7. 이 사이클의 작업 제약
|
||||
|
||||
- 애플리케이션 소스는 한 줄도 수정하지 않았다.
|
||||
- 패키지를 설치하지 않았다(`jq` 포함).
|
||||
- 사용자 워크스페이스의 빌드 산출물을 삭제하지 않았다.
|
||||
- git 상태를 변경하지 않았다 — 커밋·푸시·리셋·클린 없음.
|
||||
- 컨테이너를 기동하지 않았다. Testcontainers를 쓰는 lane은 저장소 자신의 테스트가 기동한 것이다.
|
||||
|
||||
## Source anchors
|
||||
|
||||
```
|
||||
src/config/architecture/modules.json (62개 리프의 runtime_memberships)
|
||||
src/build.gradle:477-492 (사이클 1→2 사이 변경 지점)
|
||||
src/grpc/CLAUDE.md:74-98 (grpc 블록 build-only 상태)
|
||||
src/grpc/grpc-spring-boot-starter/build.gradle:4-7 (Stable/Advanced 경계 근거)
|
||||
scripts/verify-compose-profile-contracts.sh:20-30,42-160
|
||||
src/config/runtime/compose-profile-contracts.json (15개 레인)
|
||||
|
||||
analysis/01-domain-core.md … analysis/18-app-bootstrap.md (18개 리프 SSOT)
|
||||
analysis/messaging/*.md (25개)
|
||||
analysis/grpc/*.md (18개)
|
||||
analysis/19-messaging-platform.md, analysis/20-grpc-platform.md (가족 문서, INTEGRATION_ONLY)
|
||||
|
||||
evidence/raw/325-grpc-block-is-entirely-unreachable.txt
|
||||
evidence/raw/326-grpc-stable-advanced-boundary-has-no-registry-check.txt
|
||||
evidence/raw/328-grpc-boundary-rules-never-see-real-source.txt
|
||||
evidence/raw/329-grpc-two-rule-engines-one-fed.txt
|
||||
evidence/raw/332-httpclient-dualstack-localhost-masks-tls-permanent.txt
|
||||
evidence/raw/333-eighteen-docs-source-drift-zero.txt
|
||||
evidence/raw/334-eighteen-leaf-lane-rerun.txt
|
||||
```
|
||||
Reference in New Issue
Block a user