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
+97
@@ -0,0 +1,97 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-admin-f01
|
||||
title: rejectNewAdmission() 이 단계만 기록하고 아무것도 거절하지 않는다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-admin-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-admin-f01
|
||||
file: ../../../final/evidence/rendered/grpc-admin-f01.svg
|
||||
- key: grpc-admin-f01-diagram
|
||||
file: ../../../final/assets/diagrams/grpc-admin-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-admin-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-admin.md#L131 이다.
|
||||
module: grpc-admin
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# rejectNewAdmission() 이 단계만 기록하고 아무것도 거절하지 않는다
|
||||
|
||||
javadoc 은 "Starts refusing new calls" 라고 적는다. 실제로 하는 일은 단계 목록에 표식을 넣는 것뿐이다.
|
||||
|
||||
## 문제
|
||||
|
||||
javadoc 은 "Starts refusing new calls" 라고 적는다.
|
||||
|
||||
실제로 하는 일은 단계 목록에 표식을 넣는 것뿐이다.
|
||||
|
||||
## 결론
|
||||
|
||||
조정자는 GrpcAdmissionController 를 협력자로 들고 있는데, 그것을 쓰는 곳은 inFlightAdmitted() 의 조회 하나다.
|
||||
|
||||
그리고 승인 제어기의 공개 표면에 승인을 멈추는 메서드가 없다.
|
||||
|
||||
close·drain·refuseNew 에 해당하는 것이 없다.
|
||||
|
||||
그러므로 배수가 시작된 뒤에도 tryAdmit() 은 용량이 남아 있는 한 계속 승인한다.
|
||||
|
||||
admittingNewCalls() 은 그 사실과 무관하게 거짓을 돌려준다 — 표식을 읽기 때문이다.
|
||||
|
||||
운영자나 상위 코드가 이 값을 보고 "더 이상 받지 않는다" 고 읽으면 틀린 답을 얻는다.
|
||||
|
||||
배수 테스트가 단언하는 것은 admittingNewCalls() 의 값이고, 단계 이후에 tryAdmit() 이 거절되는지는 어느 테스트도 묻지 않는다.
|
||||
|
||||
승인 제어기에 승인 중단 상태를 두고(stopAdmitting() 과 그것을 보는 tryAdmit), 조정자의 rejectNewAdmission 이 그것을 부르게 한다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcAdmissionController 참조 16건 전수 검색과 두 클래스의 공개 표면 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-admin.md#L131 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
javadoc 은 "Starts refusing new calls" 라고 적는다. 실제로 하는 일은 단계 목록에 표식을 넣는 것뿐이다.
|
||||
|
||||
## 승인 제어기에 없는 것
|
||||
|
||||
:::evidence key="grpc-admin-f01-diagram" alt="tryAdmit 과 inFlightAdmitted 가 승인 제어기 공개 표면 안에 놓이고 승인 중단 메서드가 바깥에 빗금으로 놓인다" caption="승인 제어기에 없는 것" zoom="false"
|
||||
:::
|
||||
|
||||
조정자는 `GrpcAdmissionController` 를 협력자로 들고 있는데, 그것을 쓰는 곳은 `inFlightAdmitted()` 의 조회 하나다. 그리고 승인 제어기의 공개 표면에 승인을 멈추는 메서드가 없다 — `close`·`drain`·`refuseNew` 에 해당하는 것이 없다.
|
||||
|
||||
## GrpcAdmissionController 참조 위치
|
||||
|
||||
:::evidence key="grpc-admin-f01" alt="코드베이스에서 GrpcAdmissionController 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcAdmissionController 코드베이스 검색 — 16줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 배수 이후에도 tryAdmit 은 계속 승인한다
|
||||
|
||||
용량이 남아 있는 한 그렇다. `admittingNewCalls()` 은 그 사실과 무관하게 거짓을 돌려준다 — 표식을 읽기 때문이다. 운영자나 상위 코드가 이 값을 보고 "더 이상 받지 않는다" 고 읽으면 틀린 답을 얻는다.
|
||||
|
||||
## 테스트가 묻지 않는 것
|
||||
|
||||
배수 테스트가 단언하는 것은 `admittingNewCalls()` 의 값이고, 단계 이후에 `tryAdmit()` 이 거절되는지는 어느 테스트도 묻지 않는다.
|
||||
|
||||
## 수정
|
||||
|
||||
승인 제어기에 승인 중단 상태를 두고(`stopAdmitting()` 과 그것을 보는 `tryAdmit`), 조정자의 `rejectNewAdmission` 이 그것을 부르게 한다. 지금 형태에서는 배수 순서를 지키는 장치가 순서 표식만 갖고 있다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
배수 중에 GrpcAdmissionController 를 호출해 이 결함을 재현하지 않았다. 조정자와 제어기의 공개 표면에 승인을 멈추는 메서드가 없다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-advanced-bootstrap-f01
|
||||
title: 등급 재정의에 하한이 없어 "켤 수 없다" 는 등급이 켜질 수 있다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-advanced-bootstrap-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-bootstrap-f01
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-bootstrap-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-bootstrap-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-bootstrap.md#L153 이다.
|
||||
module: grpc-advanced-bootstrap
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 등급 재정의에 하한이 없어 "켤 수 없다" 는 등급이 켜질 수 있다
|
||||
|
||||
GrpcCapabilityGrade 의 javadoc 이 두 등급을 단정한다. 그런데 등급은 런타임에 갈아끼울 수 있다.
|
||||
|
||||
## 문제
|
||||
|
||||
GrpcCapabilityGrade 의 javadoc 이 두 등급을 단정한다.
|
||||
|
||||
그런데 등급은 런타임에 갈아끼울 수 있다.
|
||||
|
||||
## 결론
|
||||
|
||||
withGrade(EDITION_2026, ADVANCED_STABLE).enable(EDITION_2026) 이면 가드의 두 번째 조건이 통과한다.
|
||||
|
||||
등급 올리기 자체는 의도된 기능이다 — 테스트 a deployment may raise a capability's grade on its own evidence 가 HEDGING(EXPERIMENTAL)을 ADVANCED_STABLE 로 올린다.
|
||||
|
||||
문제는 그 재정의에 하한이 없다는 것이다.
|
||||
|
||||
EXPERIMENTAL 을 올리는 것은 "실패 양식이 충분히 규명되지 않은 것을 감수한다" 는 판단이고 배포가 자기 증거로 내릴 수 있다.
|
||||
|
||||
WATCH 를 올리는 것은 다르다.
|
||||
|
||||
그 등급의 뜻이 "추적할 뿐 구현되지 않았다" 이므로 배포가 가질 자기 증거가 없다.
|
||||
|
||||
그리고 승격 게이트는 WATCH 가 EXPERIMENTAL 을 먼저 거쳐야 한다는 규칙을 갖는데, 런타임 재정의는 그 게이트를 지나지 않는다.
|
||||
|
||||
같은 리프 안에 문이 둘이고 증거 규칙은 한쪽에만 있다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcCapabilityGrade 참조 9건 검색과 등급 재정의 경로에 하한 가드가 있는지 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-advanced-bootstrap.md#L153 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GrpcCapabilityGrade` 의 javadoc 이 두 등급을 단정한다. 그런데 등급은 런타임에 갈아끼울 수 있다 — `withGrade(EDITION_2026, ADVANCED_STABLE).enable(EDITION_2026)` 이면 가드의 두 번째 조건이 통과한다.
|
||||
|
||||
## GrpcCapabilityGrade 참조 위치
|
||||
|
||||
:::evidence key="grpc-advanced-bootstrap-f01" alt="코드베이스에서 GrpcCapabilityGrade 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcCapabilityGrade 코드베이스 검색 — 9줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 등급 올리기 자체는 의도된 기능이다
|
||||
|
||||
테스트 `a deployment may raise a capability's grade on its own evidence` 가 `HEDGING`(EXPERIMENTAL)을 `ADVANCED_STABLE` 로 올린다.
|
||||
|
||||
## 문제는 그 재정의에 하한이 없다는 것이다
|
||||
|
||||
`EXPERIMENTAL` 을 올리는 것은 "실패 양식이 충분히 규명되지 않은 것을 감수한다" 는 판단이고 배포가 자기 증거로 내릴 수 있다. `WATCH` 를 올리는 것은 다르다 — 그 등급의 뜻이 "추적할 뿐 구현되지 않았다" 이므로 배포가 가질 자기 증거가 없다.
|
||||
|
||||
## 문이 둘이고 증거 규칙은 한쪽에만 있다
|
||||
|
||||
승격 게이트는 `WATCH` 가 `EXPERIMENTAL` 을 먼저 거쳐야 한다는 규칙을 갖는데, 런타임 재정의는 그 게이트를 지나지 않는다. 수정은 `withGrade` 가 현재 등급이 `startable()` 인 능력에만 적용되게 하거나, `WATCH`·`DISABLED` 에서 올리는 재정의를 거부하는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
등급 재정의로 그 능력을 켜는 것을 실행으로 재현하지 않았다. 코드 경로로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-advanced-bootstrap-f04
|
||||
title: capabilitiesDraggedAlong 은 독립성을 증명하지 않는다. 상수를 상수와 비교한다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-advanced-bootstrap-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-bootstrap-f04
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-bootstrap-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-bootstrap-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-bootstrap.md#L286 이다.
|
||||
module: grpc-advanced-bootstrap
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# capabilitiesDraggedAlong 은 독립성을 증명하지 않는다. 상수를 상수와 비교한다
|
||||
|
||||
javadoc 이 스스로 밝히듯 본문은 무조건 빈 목록이다. 그것을 단언하는 테스트는 리터럴이 리터럴임을 확인한다 — 증거를 능력마다 따로 기록했다는 §4 의 설계 속성과는 아무 연결이 없다.
|
||||
|
||||
## 문제
|
||||
|
||||
javadoc 이 스스로 밝히듯 본문은 무조건 빈 목록이다.
|
||||
|
||||
그것을 단언하는 테스트는 리터럴이 리터럴임을 확인한다 — 증거를 능력마다 따로 기록했다는 §4 의 설계 속성과는 아무 연결이 없다.
|
||||
|
||||
## 결론
|
||||
|
||||
설계가 무너져 apply 가 다른 능력의 등급을 바꾸게 되어도 이 메서드는 여전히 빈 목록을 돌려준다.
|
||||
|
||||
진짜 증거는 같은 테스트의 다른 줄에 있다.
|
||||
|
||||
승격을 실제로 적용하고 다른 능력의 등급이 그대로임을 확인한다.
|
||||
|
||||
이쪽은 설계가 무너지면 깨진다.
|
||||
|
||||
앞선 판에서 이 메서드를 "주석이 주장하는 대신 테스트가 붙든다"는 확인된 설계로 분류했다.
|
||||
|
||||
다시 읽으니 붙드는 것은 옆줄이고, 이 메서드는 그 옆줄이 있다는 사실을 가린다.
|
||||
|
||||
메서드를 지우고 단언을 매트릭스 비교 쪽으로 남긴다.
|
||||
|
||||
남겨 둔다면 실제로 매트릭스를 훑어 등급이 바뀐 다른 능력을 돌려주게 만든다 — 그때 비로소 이름이 하는 말과 본문이 맞는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : capabilitiesDraggedAlong 본문이 돌려주는 값과 그것을 단언하는 테스트의 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-advanced-bootstrap.md#L286 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
javadoc 이 스스로 밝히듯 `capabilitiesDraggedAlong` 의 본문은 무조건 빈 목록이다. 그것을 단언하는 테스트는 리터럴이 리터럴임을 확인한다.
|
||||
|
||||
## 본문이 무조건 빈 목록이다
|
||||
|
||||
:::evidence key="grpc-advanced-bootstrap-f04" alt="분석 문서 analysis/grpc/grpc-advanced-bootstrap.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-advanced-bootstrap.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 설계 속성과 연결이 없다
|
||||
|
||||
증거를 능력마다 따로 기록했다는 §4 의 설계 속성과 아무 연결이 없다. 설계가 무너져 `apply` 가 다른 능력의 등급을 바꾸게 되어도 이 메서드는 여전히 빈 목록을 돌려준다.
|
||||
|
||||
## 진짜 증거는 같은 테스트의 다른 줄에 있다
|
||||
|
||||
승격을 실제로 적용하고 다른 능력의 등급이 그대로임을 확인한다. 이쪽은 설계가 무너지면 깨진다.
|
||||
|
||||
## 앞선 판의 분류를 정정한다
|
||||
|
||||
앞선 판에서 이 메서드를 "주석이 주장하는 대신 테스트가 붙든다"는 확인된 설계로 분류했다. 다시 읽으니 붙드는 것은 옆줄이고, 이 메서드는 그 옆줄이 있다는 사실을 가린다. 메서드를 지우고 단언을 매트릭스 비교 쪽으로 남긴다. 남겨 둔다면 실제로 매트릭스를 훑어 등급이 바뀐 다른 능력을 돌려주게 만든다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
테스트를 실행하지 않았다. 본문이 무조건 빈 목록을 돌려주고 단언이 그 리터럴을 비교한다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-advanced-compat-f02
|
||||
title: 반응형 표면 두 타입은 테스트조차 없다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-advanced-compat-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-compat-f02
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-compat-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-compat-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-compat.md#L149 이다.
|
||||
module: grpc-advanced-compat
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 반응형 표면 두 타입은 테스트조차 없다
|
||||
|
||||
이 가족의 다른 미참조 Advanced 타입은 전부 테스트가 하나씩 있다 — 같은 패키지의 GrpcReactorCancellationBridge 는 2개 파일, GrpcReactorContextBridge 는 4개 파일에 등장한다. 두 타입은 채택자가 부를 표면이므로 production 참조 0 이 설계와 모순되지는 않는다.
|
||||
|
||||
## 문제
|
||||
|
||||
이 가족의 다른 미참조 Advanced 타입은 전부 테스트가 하나씩 있다 — 같은 패키지의 GrpcReactorCancellationBridge 는 2개 파일, GrpcReactorContextBridge 는 4개 파일에 등장한다.
|
||||
|
||||
두 타입은 채택자가 부를 표면이므로 production 참조 0 이 설계와 모순되지는 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
어긋나는 것은 검증이다.
|
||||
|
||||
채택자용 표면이면 그 계약이 무엇인지를 테스트가 붙들어야 하고, 이 가족은 다른 곳에서 정확히 그렇게 한다.
|
||||
|
||||
ReactiveGrpcClient 의 javadoc 이 "Exposes a unary call as a Mono and a server stream as a Flux" 라고 적는데, 그 사상이 취소와 배압에서 어떻게 동작하는지는 어디에서도 확인되지 않는다.
|
||||
|
||||
같은 리프의 GrpcReactorCancellationBridge 가 취소 전파를 다루므로 둘을 함께 검증할 자리가 이미 있다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcReactorCancellationBridge 참조 6건 검색과 같은 패키지 형제 타입의 테스트 등장 수 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-advanced-compat.md#L149 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
이 가족의 다른 미참조 Advanced 타입은 전부 테스트가 하나씩 있다 — 같은 패키지의 `GrpcReactorCancellationBridge` 는 2개 파일, `GrpcReactorContextBridge` 는 4개 파일에 등장한다.
|
||||
|
||||
## GrpcReactorCancellationBridge 참조 위치
|
||||
|
||||
:::evidence key="grpc-advanced-compat-f02" alt="코드베이스에서 GrpcReactorCancellationBridge 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcReactorCancellationBridge 코드베이스 검색 — 6줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## production 참조 0 은 설계와 모순되지 않는다
|
||||
|
||||
두 타입은 채택자가 부를 표면이다.
|
||||
|
||||
## 어긋나는 것은 검증이다
|
||||
|
||||
채택자용 표면이면 그 계약이 무엇인지를 테스트가 붙들어야 하고, 이 가족은 다른 곳에서 정확히 그렇게 한다. `ReactiveGrpcClient` 의 javadoc 이 "Exposes a unary call as a `Mono` and a server stream as a `Flux`" 라고 적는데, 그 사상이 취소와 배압에서 어떻게 동작하는지는 어디에서도 확인되지 않는다. 같은 리프의 `GrpcReactorCancellationBridge` 가 취소 전파를 다루므로 둘을 함께 검증할 자리가 이미 있다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
두 타입이 채택자 쪽에서 실제로 동작하는지 확인하지 않았다. 코틀린 툴체인이 없어 코틀린 계약 넷은 실행으로 확인할 수 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-advanced-compat-f03
|
||||
title: 저장소가 참조 프록시 설정을 갖고 있는데, 그것을 판정할 코드에 넣지 않는다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-advanced-compat-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-compat-f03
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-compat-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-compat-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-compat.md#L162 이다.
|
||||
module: grpc-advanced-compat
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 저장소가 참조 프록시 설정을 갖고 있는데, 그것을 판정할 코드에 넣지 않는다
|
||||
|
||||
이 리프에는 두 가지가 함께 있다. GrpcWebProxyContract.violations(profile, exposedHeaders, allowedOrigins) — 프록시 설정이 브라우저 클라이언트에게 통할지 판정하는 코드.
|
||||
|
||||
## 문제
|
||||
|
||||
이 리프에는 두 가지가 함께 있다.
|
||||
|
||||
GrpcWebProxyContract.violations(profile, exposedHeaders, allowedOrigins) — 프록시 설정이 브라우저 클라이언트에게 통할지 판정하는 코드.
|
||||
|
||||
## 결론
|
||||
|
||||
src/main/resources/envoy/envoy.yaml — 그 설정의 참조 구현.
|
||||
|
||||
그리고 설정 파일 자신이 그 관계를 주장한다.
|
||||
|
||||
GrpcWebProxyContract 는 이 파일에 대해 아무것도 단언하지 않는다.
|
||||
|
||||
판정기는 시험에서 리터럴 집합을 받고, 참조 설정은 시험에서 문자열 포함으로만 확인된다.
|
||||
|
||||
그래서 참조 설정이 grpc-status 를 노출하는지는 문자열이 확인하고, 그 노출이 충분한지 는 requiredExposedHeaders() 가 정의하는데, 둘을 잇는 코드가 없다.
|
||||
|
||||
필수 트레일러 목록이 늘어나면 판정기는 새 항목을 요구하고 참조 설정은 옛 문자열로 계속 통과한다.
|
||||
|
||||
이 리프의 다른 판정기들과 다른 점은 재료가 이미 저장소에 있다는 것이다 — grpc-testkit §17.5·grpc-server §17.1 은 스캔할 대상 자체를 만들어야 하지만, 여기서는 파일 하나를 파싱하면 된다.
|
||||
|
||||
수정은 시험이 envoy.yaml 의 expose_headers 와 allow_origin(exact:)을 뽑아 GrpcWebProxyContract.violations 에 넣고 비어 있음을 단언하는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcWebProxyContract 참조 10건 검색과 저장소의 참조 프록시 설정 위치 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-advanced-compat.md#L162 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
이 리프에는 두 가지가 함께 있다 — `GrpcWebProxyContract.violations(profile, exposedHeaders, allowedOrigins)`(프록시 설정이 브라우저 클라이언트에게 통할지 판정하는 코드)와 `src/main/resources/envoy/envoy.yaml`(그 설정의 참조 구현). 설정 파일 자신이 그 관계를 주장한다.
|
||||
|
||||
## GrpcWebProxyContract 참조 위치
|
||||
|
||||
:::evidence key="grpc-advanced-compat-f03" alt="코드베이스에서 GrpcWebProxyContract 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcWebProxyContract 코드베이스 검색 — 10줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 둘을 잇는 코드가 없다
|
||||
|
||||
판정기는 시험에서 리터럴 집합을 받고, 참조 설정은 시험에서 문자열 포함으로만 확인된다. 참조 설정이 `grpc-status` 를 노출하는지는 문자열이 확인하고, 그 노출이 **충분한지** 는 `requiredExposedHeaders()` 가 정의하는데, 둘을 잇는 코드가 없다. 필수 트레일러 목록이 늘어나면 판정기는 새 항목을 요구하고 참조 설정은 옛 문자열로 계속 통과한다.
|
||||
|
||||
## 다른 판정기들과 다른 점
|
||||
|
||||
재료가 이미 저장소에 있다 — grpc-testkit §17.5·grpc-server §17.1 은 스캔할 대상 자체를 만들어야 하지만, 여기서는 파일 하나를 파싱하면 된다. 수정은 시험이 `envoy.yaml` 의 `expose_headers` 와 `allow_origin`(`exact:`)을 뽑아 `GrpcWebProxyContract.violations` 에 넣고 비어 있음을 단언하는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
참조 프록시 설정을 실제 판정 코드에 넣어 결과를 보지 않았다. 실제 브라우저·프록시로 어떤 다리도 돌리지 않았다.
|
||||
|
||||
<!-- body:end -->
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-codegen-f01
|
||||
title: Buf 수명주기 태스크 목록이 빌드와 대조되지 않는다. 테스트는 목록을 자기 자신과 비교한다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-codegen-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-codegen-f01
|
||||
file: ../../../final/evidence/rendered/grpc-codegen-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-codegen-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-codegen.md#L190 이다.
|
||||
module: grpc-codegen
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# Buf 수명주기 태스크 목록이 빌드와 대조되지 않는다. 테스트는 목록을 자기 자신과 비교한다
|
||||
|
||||
정책이 네 태스크 이름을 담고, javadoc 이 그 이유를 적는다. 그런데 그 네 이름은 저장소의 어떤 빌드 파일에도 없다.
|
||||
|
||||
## 문제
|
||||
|
||||
정책이 네 태스크 이름을 담고, javadoc 이 그 이유를 적는다.
|
||||
|
||||
그런데 그 네 이름은 저장소의 어떤 빌드 파일에도 없다.
|
||||
|
||||
## 결론
|
||||
|
||||
그리고 테스트가 비교하는 대상이 실제 등록 태스크 집합이 아니다.
|
||||
|
||||
첫 단언은 목록을 리터럴과, 셋째는 목록을 자기 자신과 비교한다.
|
||||
|
||||
어느 것도 빌드가 그 단계를 등록했는지 묻지 않는다.
|
||||
|
||||
Buf CLI 가 이 툴체인에 없다는 것은 build.gradle 이 이미 밝힌 사실이므로 태스크가 없는 것 자체는 놀랍지 않다.
|
||||
|
||||
어긋난 것은 javadoc 의 주장이다 — 지금 형태에서 단계가 사라져도 테스트는 초록이다.
|
||||
|
||||
수정은 missingTasks 에 Gradle 이 실제로 등록한 태스크 이름 집합을 넣는 검사를 만들거나(다른 가족의 레인 등록 검사와 같은 형태), CLI 가 없는 동안에는 그 문장을 "CI 환경이 채울 계약" 으로 낮추는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 정책이 담은 네 태스크 이름을 빌드 파일 전수에서 검색하고, 테스트가 비교하는 대상 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-codegen.md#L190 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
정책이 네 태스크 이름을 담고, javadoc 이 그 이유를 적는다. 그런데 그 네 이름은 저장소의 어떤 빌드 파일에도 없다.
|
||||
|
||||
## 정책이 담은 네 태스크 이름
|
||||
|
||||
:::evidence key="grpc-codegen-f01" alt="분석 문서 analysis/grpc/grpc-codegen.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-codegen.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 테스트가 비교하는 대상이 등록 태스크 집합이 아니다
|
||||
|
||||
첫 단언은 목록을 리터럴과, 셋째는 목록을 자기 자신과 비교한다. 어느 것도 빌드가 그 단계를 등록했는지 묻지 않는다.
|
||||
|
||||
## 태스크가 없는 것 자체는 놀랍지 않다
|
||||
|
||||
Buf CLI 가 이 툴체인에 없다는 것은 build.gradle 이 이미 밝힌 사실이다. 어긋난 것은 javadoc 의 주장이다 — 지금 형태에서 단계가 사라져도 테스트는 초록이다. 수정은 `missingTasks` 에 Gradle 이 실제로 등록한 태스크 이름 집합을 넣는 검사를 만들거나, CLI 가 없는 동안에는 그 문장을 "CI 환경이 채울 계약" 으로 낮추는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제 protoc 이나 Buf CLI 를 돌리지 않았다. 저장소에 둘 다 없다. 리플렉션이나 서비스 로더로 부르는 형태는 배제하지 못했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-codegen-f04
|
||||
title: sha256: 검사가 길이 15자 이상만 요구한다. 저장소 자신의 테스트가 32자 해시를 통과시킨다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-codegen-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-codegen-f04
|
||||
file: ../../../final/evidence/rendered/grpc-codegen-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-codegen-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-codegen.md#L287 이다.
|
||||
module: grpc-codegen
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# sha256: 검사가 길이 15자 이상만 요구한다. 저장소 자신의 테스트가 32자 해시를 통과시킨다
|
||||
|
||||
같은 검사가 두 곳에 손으로 복사돼 있다. "sha256:" 이 7자이므로 뒤에 8자만 있으면 통과한다.
|
||||
|
||||
## 문제
|
||||
|
||||
같은 검사가 두 곳에 손으로 복사돼 있다.
|
||||
|
||||
"sha256:" 이 7자이므로 뒤에 8자만 있으면 통과한다.
|
||||
|
||||
## 결론
|
||||
|
||||
sha256 digest 는 hex 64자다.
|
||||
|
||||
그리고 이 헐거움이 테스트에 이미 드러나 있다.
|
||||
|
||||
32자 — sha256 이 아니다.
|
||||
|
||||
여기서는 "다른 해시" 역할이라 결과가 바뀌지 않지만, 형식 검사가 이런 값을 유효한 해시로 받는다는 사실 자체가 이 값 객체의 주장("the hashes that prove which bytes it was built from")을 약하게 만든다.
|
||||
|
||||
sha256: 뒤 64자 hex 를 정규식으로 요구하고, 검사를 한 곳에 둔다 — 두 record 가 같은 규칙을 각자 적고 있는 지금 형태에서는 한쪽만 조여도 다른 쪽이 남는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 두 곳에 복사된 sha256 검사의 길이 조건과 저장소 테스트의 해시 리터럴 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-codegen.md#L287 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
같은 검사가 두 곳에 손으로 복사돼 있다. `"sha256:"` 이 7자이므로 뒤에 8자만 있으면 통과한다. sha256 digest 는 hex 64자다.
|
||||
|
||||
## 길이 검사가 요구하는 최소
|
||||
|
||||
:::evidence key="grpc-codegen-f04" alt="분석 문서 analysis/grpc/grpc-codegen.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-codegen.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 헐거움이 테스트에 이미 드러나 있다
|
||||
|
||||
테스트가 쓰는 값이 32자 — sha256 이 아니다. 여기서는 "다른 해시" 역할이라 결과가 바뀌지 않지만, 형식 검사가 이런 값을 유효한 해시로 받는다는 사실 자체가 이 값 객체의 주장("the hashes that prove which bytes it was built from")을 약하게 만든다.
|
||||
|
||||
## 수정
|
||||
|
||||
`sha256:` 뒤 64자 hex 를 정규식으로 요구하고, 검사를 한 곳에 둔다 — 두 record 가 같은 규칙을 각자 적고 있는 지금 형태에서는 한쪽만 조여도 다른 쪽이 남는다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
짧은 해시를 실제 발행 경로에 넣어 통과를 관측하지 않았다. 길이 조건과 테스트 리터럴의 대조로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-core-api-f02
|
||||
title: 모듈 목록 테스트가 레지스트리와 목록을 붙들지 않는다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-core-api-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-core-api-f02
|
||||
file: ../../../final/evidence/rendered/grpc-core-api-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-core-api-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-core-api.md#L157 이다.
|
||||
module: grpc-core-api
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 모듈 목록 테스트가 레지스트리와 목록을 붙들지 않는다
|
||||
|
||||
클래스 javadoc 이 두 SSOT 의 관계를 적는다. 그 테스트는 레지스트리를 읽지 않는다.
|
||||
|
||||
## 문제
|
||||
|
||||
클래스 javadoc 이 두 SSOT 의 관계를 적는다.
|
||||
|
||||
그 테스트는 레지스트리를 읽지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
다섯 테스트가 하는 일은 목록을 리터럴과 대조하고, 두 집합의 서로소를 확인하고, 누출 판정을 확인하는 것이다.
|
||||
|
||||
modules.json 을 읽는 줄도, 파일 경로도 없다.
|
||||
|
||||
두 목록은 오늘 일치한다 — 레지스트리의 grpc 계열 리프가 18 개이고 목록이 12 + 6 이다.
|
||||
|
||||
어긋난 것은 그 일치를 무엇이 지키는가다.
|
||||
|
||||
같은 저장소가 이 형태를 messaging 가족에서 이미 기록했다 — 정확한 목록은 레지스트리가 소유하므로 산문에서 세지 않는다, 세는 순간 다시 표류한다.
|
||||
|
||||
수정은 테스트가 modules.json 을 읽어 grpc 계열 리프 집합과 두 상수 집합의 합집합을 대조하는 것이다.
|
||||
|
||||
그 테스트가 있으면 새 리프가 어느 쪽에도 들어가지 않은 채 추가되는 것을 잡는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 테스트가 읽는 입력과 클래스 javadoc 이 선언한 두 SSOT 의 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-core-api.md#L157 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
클래스 javadoc 이 두 SSOT 의 관계를 적는다 — 레지스트리(`src/config/architecture/modules.json`)가 어떤 Gradle 프로젝트가 존재하는지의 SSOT 이고, 이 목록은 그중 Stable 계약이 덮는 것의 SSOT 다.
|
||||
|
||||
## javadoc 이 적은 두 SSOT 의 관계
|
||||
|
||||
:::evidence key="grpc-core-api-f02" alt="분석 문서 analysis/grpc/grpc-core-api.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-core-api.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 그 테스트는 레지스트리를 읽지 않는다
|
||||
|
||||
다섯 테스트가 하는 일은 목록을 리터럴과 대조하고, 두 집합의 서로소를 확인하고, 누출 판정을 확인하는 것이다. `modules.json` 을 읽는 줄도, 파일 경로도 없다.
|
||||
|
||||
## 두 목록은 오늘 일치한다
|
||||
|
||||
레지스트리의 grpc 계열 리프가 18 개이고 목록이 12 + 6 이다. 어긋난 것은 그 일치를 무엇이 지키는가다.
|
||||
|
||||
## 같은 저장소가 이 형태를 messaging 가족에서 이미 기록했다
|
||||
|
||||
정확한 목록은 레지스트리가 소유하므로 산문에서 세지 않는다 — 세는 순간 다시 표류한다. 수정은 테스트가 `modules.json` 을 읽어 grpc 계열 리프 집합과 두 상수 집합의 합집합을 대조하는 것이다. 그 테스트가 있으면 새 리프가 어느 쪽에도 들어가지 않은 채 추가되는 것을 잡는다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
레지스트리와 목록을 어긋나게 만들어 테스트가 통과하는지 실행으로 확인하지 않았다. 테스트가 레지스트리를 읽지 않는다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-server-f01
|
||||
title: 두 아키텍처 규칙이 저장소 소스에 적용되지 않는다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-server-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-server-f01
|
||||
file: ../../../final/evidence/rendered/grpc-server-f01.svg
|
||||
- key: grpc-server-f01-diagram
|
||||
file: ../../../final/assets/diagrams/grpc-server-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-server-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-server.md#L140 이다.
|
||||
module: grpc-server
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 두 아키텍처 규칙이 저장소 소스에 적용되지 않는다
|
||||
|
||||
GrpcApplicationBoundaryRules javadoc: 세 적용처 중 저장소에 존재하는 것이 없다. 그리고 이 리프의 테스트는 저장소 파일을 훑지 않는다.
|
||||
|
||||
## 문제
|
||||
|
||||
GrpcApplicationBoundaryRules javadoc: 세 적용처 중 저장소에 존재하는 것이 없다.
|
||||
|
||||
그리고 이 리프의 테스트는 저장소 파일을 훑지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
인라인 소스 문자열을 넣는다.
|
||||
|
||||
즉 규칙의 판정 로직은 검증되지만, 저장소의 어떤 파일도 그 판정을 받지 않는다.
|
||||
|
||||
GrpcServiceAdapterMarker 는 규칙이 어댑터를 런타임에 열거할 수 있도록 만든 애너테이션인데, 그것을 붙인 타입도 그것을 읽는 코드도 없다.
|
||||
|
||||
이 리프의 테스트에 저장소 소스를 훑는 검사를 추가한다 — src/**/*.java 를 읽어 GrpcRawApiImportRule.violations 를 돌리고 비어 있음을 단언하는 형태다.
|
||||
|
||||
규칙이 이미 파일 이름과 소스 텍스트를 받는 서명이므로 재료는 갖춰져 있다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcApplicationBoundaryRules 참조 8건 검색과 규칙이 판정하는 입력의 출처 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-server.md#L140 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GrpcApplicationBoundaryRules` javadoc 이 드는 세 적용처 중 저장소에 존재하는 것이 없다.
|
||||
|
||||
## 규칙이 닿지 않는 대상
|
||||
|
||||
:::evidence key="grpc-server-f01-diagram" alt="인라인 소스 문자열만 규칙이 판정하는 입력 안에 놓이고 저장소의 자바 파일이 바깥에 빗금으로 놓인다" caption="규칙이 닿지 않는 대상" zoom="false"
|
||||
:::
|
||||
|
||||
그리고 이 리프의 테스트는 저장소 파일을 훑지 않는다 — 인라인 소스 문자열을 넣는다. 즉 규칙의 판정 로직은 검증되지만, 저장소의 어떤 파일도 그 판정을 받지 않는다.
|
||||
|
||||
## GrpcApplicationBoundaryRules 참조 위치
|
||||
|
||||
:::evidence key="grpc-server-f01" alt="코드베이스에서 GrpcApplicationBoundaryRules 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcApplicationBoundaryRules 코드베이스 검색 — 8줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 애너테이션도 붙은 곳이 없다
|
||||
|
||||
`GrpcServiceAdapterMarker` 는 규칙이 어댑터를 런타임에 열거할 수 있도록 만든 애너테이션인데, 그것을 붙인 타입도 그것을 읽는 코드도 없다.
|
||||
|
||||
## 재료는 갖춰져 있다
|
||||
|
||||
이 리프의 테스트에 저장소 소스를 훑는 검사를 추가한다 — `src/**/*.java` 를 읽어 `GrpcRawApiImportRule.violations` 를 돌리고 비어 있음을 단언하는 형태다. 규칙이 이미 파일 이름과 소스 텍스트를 받는 서명이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제 서버를 세워 인터셉터 사슬을 돌리지 않았다. 이 리프에 서버를 만드는 코드가 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-testkit-f01
|
||||
title: 네 레인이 check 에 붙지 않고, 이 가족을 이름으로 부르는 워크플로가 없다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-testkit-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-testkit-f01
|
||||
file: ../../../final/evidence/rendered/grpc-testkit-f01.svg
|
||||
- key: grpc-testkit-f01-diagram
|
||||
file: ../../../final/assets/diagrams/grpc-testkit-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-testkit-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-testkit.md#L153 이다.
|
||||
module: grpc-testkit
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 네 레인이 check 에 붙지 않고, 이 가족을 이름으로 부르는 워크플로가 없다
|
||||
|
||||
ca.strict-test-lane.gradle 은 레인을 verification 그룹의 Test 태스크로 등록만 한다. check 에 연결하는 줄이 없다.
|
||||
|
||||
## 문제
|
||||
|
||||
ca.strict-test-lane.gradle 은 레인을 verification 그룹의 Test 태스크로 등록만 한다.
|
||||
|
||||
check 에 연결하는 줄이 없다.
|
||||
|
||||
## 결론
|
||||
|
||||
그리고 CI 워크플로에서 이 가족을 이름으로 부르는 것이 없다.
|
||||
|
||||
ci-quality-gates.yml 이 ./gradlew check 를 돌리므로 각 리프의 기본 test 는 돈다(이번에 확인: classes=71 tests=579 failures=0).
|
||||
|
||||
네 증거 레인은 그 밖에 있다.
|
||||
|
||||
결과적으로 이 플랫폼의 CONTRACT·TRANSPORT·FAULT 등급을 뒷받침하는 것은 25개 테스트이고, 그 25개는 누군가 명령을 직접 입력할 때만 돈다.
|
||||
|
||||
build.gradle 자신이 그 위험을 적는다 — "A lane that discovers nothing fails, and none of them can serve an up-to-date result".
|
||||
|
||||
첫 성질은 레인 규약이 지킨다.
|
||||
|
||||
둘째 성질은 아무도 돌리지 않으면 무의미하다.
|
||||
|
||||
같은 저장소가 이 형태를 두 번 기록했다 — 모듈 18 의 "붉은 게이트는 마지막으로 돌린 사람이 본 것을 보고한다" 와 mongo 가족의 릴리스 게이트 지형.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 레인 등록 코드와 check 연결 여부 확인, 이 가족을 부르는 워크플로 검색
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-testkit.md#L153 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`ca.strict-test-lane.gradle` 은 레인을 `verification` 그룹의 `Test` 태스크로 **등록만** 한다. `check` 에 연결하는 줄이 없다.
|
||||
|
||||
## 레인을 부르는 경로
|
||||
|
||||
:::evidence key="grpc-testkit-f01-diagram" alt="직접 입력한 명령만 레인을 부르는 경로 안에 놓이고 check 연결과 CI 워크플로가 바깥에 빗금으로 놓인다" caption="레인을 부르는 경로" zoom="false"
|
||||
:::
|
||||
|
||||
그리고 CI 워크플로에서 이 가족을 이름으로 부르는 것이 없다. `ci-quality-gates.yml` 이 `./gradlew check` 를 돌리므로 각 리프의 기본 `test` 는 돈다(이번에 확인: classes=71 tests=579 failures=0). 네 증거 레인은 그 밖에 있다.
|
||||
|
||||
## 레인이 등록되는 방식
|
||||
|
||||
:::evidence key="grpc-testkit-f01" alt="분석 문서 analysis/grpc/grpc-testkit.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-testkit.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 25개 테스트가 누군가 명령을 입력할 때만 돈다
|
||||
|
||||
이 플랫폼의 CONTRACT·TRANSPORT·FAULT 등급을 뒷받침하는 것이 그 25개다. build.gradle 자신이 위험을 적는다 — "A lane that discovers nothing fails, and **none of them can serve an up-to-date result**". 첫 성질은 레인 규약이 지킨다. 둘째 성질은 아무도 돌리지 않으면 무의미하다.
|
||||
|
||||
## 같은 저장소가 이 형태를 두 번 기록했다
|
||||
|
||||
모듈 18 의 "붉은 게이트는 마지막으로 돌린 사람이 본 것을 보고한다" 와 mongo 가족의 릴리스 게이트 지형. 차이는 이쪽 레인이 오늘 초록이라는 것이고, 그것을 확인한 방법이 이번 분석에서 직접 돌린 것이라는 점이다. 수정은 세 레인(성능 제외)을 `check` 에 붙이거나, messaging 가족처럼 전용 워크플로를 두는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
성능 레인을 돌리지 않았다. 세 레인은 이전 분석에서 통과를 확인했고 이번 회차에는 다시 돌리지 않았다.
|
||||
|
||||
<!-- body:end -->
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-testkit-f03
|
||||
title: 고장 레인의 유일한 실소켓 시험이 자기가 관측한 것을 버리고 리터럴로 증거를 만든다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-testkit-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-testkit-f03
|
||||
file: ../../../final/evidence/rendered/grpc-testkit-f03.svg
|
||||
- key: grpc-testkit-f03-diagram
|
||||
file: ../../../final/assets/diagrams/grpc-testkit-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-testkit-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-testkit.md#L187 이다.
|
||||
module: grpc-testkit
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 고장 레인의 유일한 실소켓 시험이 자기가 관측한 것을 버리고 리터럴로 증거를 만든다
|
||||
|
||||
GrpcTransportEvidenceClassifierTest.aRealConnectionLossAfterAppStartIsCompletionUnknown 은 이 리프에서 유일하게 실제 연결을 작업 중에 끊는다. 서버 핸들러가 래치로 멈춰 있는 동안 server.close() 를 부른다.
|
||||
|
||||
## 문제
|
||||
|
||||
GrpcTransportEvidenceClassifierTest.aRealConnectionLossAfterAppStartIsCompletionUnknown 은 이 리프에서 유일하게 실제 연결을 작업 중에 끊는다.
|
||||
|
||||
서버 핸들러가 래치로 멈춰 있는 동안 server.close() 를 부른다.
|
||||
|
||||
## 결론
|
||||
|
||||
거기까지는 진짜 고장이다.
|
||||
|
||||
그런데 그 고장이 만들어 낸 관측이 어디에도 남지 않는다.
|
||||
|
||||
주석이 "the exception is the observation" 이라고 말하는데 그 예외는 catch 안에서 사라지고, 변수는 null 로 고정되고, 단언은 자기 대입을 확인한다.
|
||||
|
||||
그리고 GrpcFaultResult 에 들어가는 증거는 방금 일어난 호출에서 오지 않는다.
|
||||
|
||||
즉 소켓은 실제로 죽었고, 그 죽음에서 읽어 낸 값은 하나도 쓰이지 않는다.
|
||||
|
||||
이 시험이 실제로 증명하는 것은 applicationStarted == true 하나다.
|
||||
|
||||
나머지는 분류기의 산술이고, 그것은 같은 파일의 다른 일곱 시험이 이미 소켓 없이 증명한다.
|
||||
|
||||
이 형태를 이 리프 자신이 이름 붙여 두었다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcTransportEvidenceClassifierTest 참조 검색과 관측값·단언값의 대입 경로 추적
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-testkit.md#L187 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GrpcTransportEvidenceClassifierTest.aRealConnectionLossAfterAppStartIsCompletionUnknown` 은 이 리프에서 유일하게 실제 연결을 작업 중에 끊는다. 서버 핸들러가 래치로 멈춰 있는 동안 `server.close()` 를 부른다. 거기까지는 진짜 고장이다.
|
||||
|
||||
## 증거가 오는 곳
|
||||
|
||||
:::evidence key="grpc-testkit-f03-diagram" alt="리터럴로 쓴 값만 증거가 오는 곳 안에 놓이고 실제 소켓 관측이 바깥에 빗금으로 놓인다" caption="증거가 오는 곳" zoom="false"
|
||||
:::
|
||||
|
||||
그 고장이 만들어 낸 관측이 어디에도 남지 않는다. 주석이 "the exception is the observation" 이라고 말하는데 그 예외는 `catch` 안에서 사라지고, 변수는 `null` 로 고정되고, 단언은 자기 대입을 확인한다. 그리고 `GrpcFaultResult` 에 들어가는 증거는 방금 일어난 호출에서 오지 않는다.
|
||||
|
||||
## GrpcTransportEvidenceClassifierTest 참조 위치
|
||||
|
||||
:::evidence key="grpc-testkit-f03" alt="코드베이스에서 GrpcTransportEvidenceClassifierTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcTransportEvidenceClassifierTest 코드베이스 검색 — 1줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 이 시험이 실제로 증명하는 것
|
||||
|
||||
`applicationStarted == true` 하나다. 나머지는 분류기의 산술이고, 그것은 같은 파일의 다른 일곱 시험이 이미 소켓 없이 증명한다.
|
||||
|
||||
## 한 단계 아래의 같은 치환이다
|
||||
|
||||
이 형태를 이 리프 자신이 이름 붙여 두었다. 여기서는 소켓이 열렸는데도 등급을 뒷받침해야 할 증거가 여전히 손으로 쓴 값이다. 수정은 `callUnary` 를 부른 스레드가 잡은 예외와 그 시점의 진행 상태를 밖으로 넘겨(`AtomicReference`) 그것으로 `ClientObservation` 을 구성하는 것이다 — 그러면 `sendCompleted`·`responseHeadersReceived` 가 관측값이 되고 이 시험이 FAULT 등급을 실제로 뒷받침한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
observedFailure 경로를 실행으로 확인하지 않았다. 대입과 단언이 같은 메서드 안에 있고 그 사이에 재대입이 없다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-testkit-f04
|
||||
title: 호환성 표의 레인 이름과 빌드의 레인 이름이 서로 다른 집합이다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-testkit-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-testkit-f04
|
||||
file: ../../../final/evidence/rendered/grpc-testkit-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-testkit-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-testkit.md#L233 이다.
|
||||
module: grpc-testkit
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 호환성 표의 레인 이름과 빌드의 레인 이름이 서로 다른 집합이다
|
||||
|
||||
GrpcStableReleaseGate.evaluate 의 둘째 인자는 Map<String, Boolean> laneResults 이고, GrpcCompatibilityMatrix.missingResults 가 그 키를 자기 목록과 대조한다. 그 목록은 배포 조합의 이름이다 — boot-managed-platform, netty-shaded, upstream-grpc-java-override, protobuf-edition-2024 … 빌드가 등록하는 레인의 이름은 증거 종류다 — grpcInProcessContractTest, grpcNettyContractTest, grpcFaultTest, grpcPerformanceTest.
|
||||
|
||||
## 문제
|
||||
|
||||
GrpcStableReleaseGate.evaluate 의 둘째 인자는 Map<String, Boolean> laneResults 이고, GrpcCompatibilityMatrix.missingResults 가 그 키를 자기 목록과 대조한다.
|
||||
|
||||
그 목록은 배포 조합의 이름이다 — boot-managed-platform, netty-shaded, upstream-grpc-java-override, protobuf-edition-2024 … 빌드가 등록하는 레인의 이름은 증거 종류다 — grpcInProcessContractTest, grpcNettyContractTest, grpcFaultTest, grpcPerformanceTest.
|
||||
|
||||
## 결론
|
||||
|
||||
두 집합의 교집합이 비어 있다.
|
||||
|
||||
그래서 §17.1 을 고쳐 네 Gradle 레인을 check 에 붙이더라도, 그 결과가 이 게이트의 laneResults 를 채우지는 못한다 — 이름이 다른 축을 가리키기 때문이다.
|
||||
|
||||
게이트가 요구하는 것은 "Boot 관리 플랫폼 조합에서 돌았는가" 이고, 레인이 답할 수 있는 것은 "전송 증거를 냈는가" 다.
|
||||
|
||||
두 축이 다 필요하다는 것 자체는 옳다.
|
||||
|
||||
기록하는 이유는 §17.1·§17.2 의 수정이 이것까지 함께 다루지 않으면 게이트가 여전히 손으로 만든 값을 먹는다는 점이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcStableReleaseGate 참조 7건 검색과 두 레인 이름 집합의 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-testkit.md#L233 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GrpcStableReleaseGate.evaluate` 의 둘째 인자는 `Map<String, Boolean> laneResults` 이고, `GrpcCompatibilityMatrix.missingResults` 가 그 키를 자기 목록과 대조한다.
|
||||
|
||||
## GrpcStableReleaseGate 참조 위치
|
||||
|
||||
:::evidence key="grpc-testkit-f04" alt="코드베이스에서 GrpcStableReleaseGate 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcStableReleaseGate 코드베이스 검색 — 7줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 두 집합의 교집합이 비어 있다
|
||||
|
||||
목록은 배포 조합의 이름이다 — `boot-managed-platform`, `netty-shaded`, `upstream-grpc-java-override`, `protobuf-edition-2024` …. 빌드가 등록하는 레인의 이름은 증거 종류다 — `grpcInProcessContractTest`, `grpcNettyContractTest`, `grpcFaultTest`, `grpcPerformanceTest`.
|
||||
|
||||
## 그래서 §17.1 을 고쳐도 채워지지 않는다
|
||||
|
||||
네 Gradle 레인을 `check` 에 붙이더라도 그 결과가 이 게이트의 `laneResults` 를 채우지는 못한다 — 이름이 다른 축을 가리키기 때문이다. 게이트가 요구하는 것은 "Boot 관리 플랫폼 조합에서 돌았는가" 이고, 레인이 답할 수 있는 것은 "전송 증거를 냈는가" 다.
|
||||
|
||||
## 두 축이 다 필요하다는 것 자체는 옳다
|
||||
|
||||
기록하는 이유는 §17.1·§17.2 의 수정이 이것까지 함께 다루지 않으면 게이트가 여전히 손으로 만든 값을 먹는다는 점이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
두 집합을 실제로 맞물려 게이트를 실행하지 않았다. 이름 목록의 대조로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-admin-api-f01
|
||||
title: DestructiveOperationGuard 의 두 분기가 문서에도 없고 테스트에도 없다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-admin-api-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-api-f01
|
||||
file: ../../../final/evidence/rendered/messaging-admin-api-f01.svg
|
||||
- key: messaging-admin-api-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-admin-api-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-api-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-api.md#L915 이다.
|
||||
module: messaging-admin-api
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# DestructiveOperationGuard 의 두 분기가 문서에도 없고 테스트에도 없다
|
||||
|
||||
operation 불일치(:72-83)와 source 불일치(:84-93)는 클래스 javadoc 의 "Four conditions" 에 포함되지 않고, 두 에러 코드를 단언하는 테스트도 저장소 전체에 없다(EVD-303). 이 둘은 사소한 검사가 아니다 — 5번 분기의 인라인 주석이 정확히 말한다: "A guard that only checks presence and window lets a verified redrive approval authorise a destination deletion." 즉 검증된 승인으로 목적지 삭제를 인가하는 것을 막는 검사다.
|
||||
|
||||
## 문제
|
||||
|
||||
operation 불일치(:72-83)와 source 불일치(:84-93)는 클래스 javadoc 의 "Four conditions" 에 포함되지 않고, 두 에러 코드를 단언하는 테스트도 저장소 전체에 없다(EVD-303).
|
||||
|
||||
이 둘은 사소한 검사가 아니다 — 5번 분기의 인라인 주석이 정확히 말한다: "A guard that only checks presence and window lets a verified redrive approval authorise a destination deletion." 즉 검증된 승인으로 목적지 삭제를 인가하는 것을 막는 검사다.
|
||||
|
||||
## 결론
|
||||
|
||||
혼동을 키우는 정황이 하나 더 있다.
|
||||
|
||||
anApplicationRuntimeCannotRedrive 는 REDRIVE 요청에 REPLAY 승인을 넘기지만 adminCredentialPresent=false 라 분기 2에서 먼저 걸린다.
|
||||
|
||||
불일치 조합이 테스트에 등장하지만 그 분기는 실행되지 않는다.
|
||||
|
||||
수정: javadoc 을 여섯으로 고치고, new DestructiveOperationGuard(true) 위에서 operation 불일치·source 불일치 각각 1건씩 테스트를 추가한다.
|
||||
|
||||
이 리프에는 이미 verified(operation, source, from, until) 헬퍼가 있어 두 줄이면 된다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : DestructiveOperationGuard 참조 19건 검색과 javadoc 이 든 조건 대비 본문 분기 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-admin-api.md#L915 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
operation 불일치(`:72-83`)와 source 불일치(`:84-93`)는 클래스 javadoc 의 "Four conditions" 에 포함되지 않고, 두 에러 코드를 단언하는 테스트도 저장소 전체에 없다(`EVD-303`).
|
||||
|
||||
## 목록에서 빠진 검사
|
||||
|
||||
:::evidence key="messaging-admin-api-f01-diagram" alt="javadoc 이 든 조건 쪽에 승인 존재와 유효 기간과 빠진 둘이 놓이고 본문이 보는 조건 쪽에 작업 종류와 출처가 놓인다" caption="목록에서 빠진 검사" zoom="false"
|
||||
:::
|
||||
|
||||
## 사소한 검사가 아니다
|
||||
|
||||
5번 분기의 인라인 주석이 정확히 말한다 — "A guard that only checks presence and window lets a verified redrive approval authorise a destination deletion." 즉 **검증된 승인으로 목적지 삭제를 인가하는 것**을 막는 검사다.
|
||||
|
||||
## DestructiveOperationGuard 참조 위치
|
||||
|
||||
:::evidence key="messaging-admin-api-f01" alt="코드베이스에서 DestructiveOperationGuard 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DestructiveOperationGuard 코드베이스 검색 — 19줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 혼동을 키우는 정황
|
||||
|
||||
`anApplicationRuntimeCannotRedrive` 는 `REDRIVE` 요청에 `REPLAY` 승인을 넘기지만 `adminCredentialPresent=false` 라 분기 2에서 먼저 걸린다. 불일치 조합이 테스트에 등장하지만 그 분기는 실행되지 않는다.
|
||||
|
||||
## 수정
|
||||
|
||||
javadoc 을 여섯으로 고치고, `new DestructiveOperationGuard(true)` 위에서 operation 불일치·source 불일치 각각 1건씩 테스트를 추가한다. 이 리프에는 이미 `verified(operation, source, from, until)` 헬퍼가 있어 두 줄이면 된다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
guard 를 실제로 호출해 두 분기에 도달시키지 않았다. 두 에러 코드를 단언하는 테스트가 저장소 전체에 없다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-cloudevents-f01
|
||||
title: 상호운용을 위한 매퍼가 명세 준수 이벤트를 분류되지 않은 예외로 거절한다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-cloudevents-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-cloudevents-f01
|
||||
file: ../../../final/evidence/rendered/messaging-cloudevents-f01.svg
|
||||
- key: messaging-cloudevents-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-cloudevents-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-cloudevents-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-cloudevents.md#L518 이다.
|
||||
module: messaging-cloudevents
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 상호운용을 위한 매퍼가 명세 준수 이벤트를 분류되지 않은 예외로 거절한다
|
||||
|
||||
fromCloudEvent가 new MessageId(UUID.fromString(event.getId()))로 id를 파싱한다. CloudEvents 1.0.2는 id를 비어 있지 않은 문자열로만 제약한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
fromCloudEvent가 new MessageId(UUID.fromString(event.getId()))로 id를 파싱한다.
|
||||
|
||||
CloudEvents 1.0.2는 id를 비어 있지 않은 문자열로만 제약한다.
|
||||
|
||||
## 결론
|
||||
|
||||
런타임 probe 결과: 명세 예시 id A234-1234-1234 → java.lang.IllegalArgumentException: Invalid UUID string, UUIDv4 → java.lang.IllegalArgumentException: a message identity is UUIDv7.
|
||||
|
||||
둘 다 MessagingException이 아니다.
|
||||
|
||||
(1) 범위.
|
||||
|
||||
v4 거절은 의도이고 테스트 주석이 그렇게 적는다.
|
||||
|
||||
그러나 비UUID 거절은 어디에도 언급되지 않았고 그것은 다른 판단이다 — v4 거절은 "우리 정책", 비UUID 거절은 "CloudEvents 상호운용 포기"다.
|
||||
|
||||
이 leaf의 존재 이유가 상호운용인데 명세 예시조차 받지 못한다.
|
||||
|
||||
(2) 실패 어휘.
|
||||
|
||||
같은 메서드의 다른 검증 실패 넷은 전부 MessageValidationException이고 안정 코드(CLOUDEVENT_TIME_REQUIRED 등)를 갖는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : MessagingException 참조 36건 검색과 id 파싱 경로의 예외 분류 확인. 런타임 probe 로 거절을 실행 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-cloudevents.md#L518 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`fromCloudEvent`가 `new MessageId(UUID.fromString(event.getId()))`로 id를 파싱한다. CloudEvents 1.0.2는 `id`를 비어 있지 않은 문자열로만 제약한다.
|
||||
|
||||
## 구현이 받는 id 범위
|
||||
|
||||
:::evidence key="messaging-cloudevents-f01-diagram" alt="UUIDv7 id 만 구현이 받는 범위 안에 놓이고 명세 예시 id 와 UUIDv4 id 가 바깥에 빗금으로 놓인다" caption="구현이 받는 id 범위" zoom="false"
|
||||
:::
|
||||
|
||||
런타임 probe 결과 — 명세 예시 id `A234-1234-1234` → `java.lang.IllegalArgumentException: Invalid UUID string`, UUIDv4 → `java.lang.IllegalArgumentException: a message identity is UUIDv7`. **둘 다 `MessagingException`이 아니다.**
|
||||
|
||||
## MessagingException 참조 위치
|
||||
|
||||
:::evidence key="messaging-cloudevents-f01" alt="코드베이스에서 MessagingException 를 검색한 출력 36줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingException 코드베이스 검색 — 36줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 범위 — 두 거절의 성격이 다르다
|
||||
|
||||
v4 거절은 의도이고 테스트 주석이 그렇게 적는다. 그러나 **비UUID 거절은 어디에도 언급되지 않았고** 그것은 다른 판단이다 — v4 거절은 "우리 정책", 비UUID 거절은 "CloudEvents 상호운용 포기"다. 이 leaf의 존재 이유가 상호운용인데 명세 예시조차 받지 못한다.
|
||||
|
||||
## 실패 어휘 — id 실패만 분류되지 않는다
|
||||
|
||||
같은 메서드의 다른 검증 실패 넷은 전부 `MessageValidationException`이고 안정 코드(`CLOUDEVENT_TIME_REQUIRED` 등)를 갖는다. id 실패만 raw `IllegalArgumentException`이라 `FailureDescriptor`가 없다 — 카테고리도, 코드도, retryable 판정도 없다. DLQ 라우팅과 대시보드가 이 실패를 분류하지 못한다. 같은 문제가 `causationid`·`type`·`correlationid`·`tenantcontext`·`producer`·`datacontenttype`·`schemaversion` 값 범위에도 있다(§6의 "코드 없음" 여덟 행).
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제 외부 CloudEvents producer 의 id 형식 분포를 측정하지 않았다. 명세가 형식을 제약하지 않으므로 UUID 가 아닐 가능성이 높다는 것까지만 말할 수 있다.
|
||||
|
||||
<!-- body:end -->
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-inbox-jdbc-postgresql-f02
|
||||
title: 속성을 이름으로 주장하는 테스트가 그 속성을 보일 수 없는 fake 위에서 통과한다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-inbox-jdbc-postgresql-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-inbox-jdbc-postgresql-f02
|
||||
file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-f02.svg
|
||||
- key: messaging-inbox-jdbc-postgresql-f02-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-inbox-jdbc-postgresql-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-inbox-jdbc-postgresql.md#L657 이다.
|
||||
module: messaging-inbox-jdbc-postgresql
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 속성을 이름으로 주장하는 테스트가 그 속성을 보일 수 없는 fake 위에서 통과한다
|
||||
|
||||
InboxOperationsTest.cleanupDeletesInBoundedBatches가 InMemoryInbox(List.of(1000, 500))에 대해 removed == 1500과 cutoffs.hasSize(3)을 단언한다. 그 fake의 무제한 메서드는 미리 준 목록을 순서대로 반환하는 대본이고 아무것도 삭제하거나 제한하지 않는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
InboxOperationsTest.cleanupDeletesInBoundedBatches가 InMemoryInbox(List.of(1000, 500))에 대해 removed == 1500과 cutoffs.hasSize(3)을 단언한다.
|
||||
|
||||
그 fake의 무제한 메서드는 미리 준 목록을 순서대로 반환하는 대본이고 아무것도 삭제하거나 제한하지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
bounded 오버로드는 fake에도 있지만 job이 부르지 않아 실행되지 않는다.
|
||||
|
||||
이 테스트가 통과로 증명하는 것은 "0을 받을 때까지 루프를 돈다"이고 이름이 주장하는 "배치로 제한된다"가 아니다.
|
||||
|
||||
1000·500은 배치처럼 보이는 숫자다.
|
||||
|
||||
P1이 이 테스트를 통과한 채로 존재할 수 있었던 이유다.
|
||||
|
||||
그리고 컨테이너 레인(InboxPostgresIT.retentionRemovesOldRows)도 무제한 오버로드를 한 행에 대해 부르므로 실 DB에서도 드러나지 않는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : InboxOperationsTest 참조 검색과 테스트가 쓰는 fake 의 무제한 메서드 구현 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-inbox-jdbc-postgresql.md#L657 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`InboxOperationsTest.cleanupDeletesInBoundedBatches`가 `InMemoryInbox(List.of(1000, 500))`에 대해 `removed == 1500`과 `cutoffs.hasSize(3)`을 단언한다.
|
||||
|
||||
## 대역이 재현하지 못하는 것
|
||||
|
||||
:::evidence key="messaging-inbox-jdbc-postgresql-f02-diagram" alt="실제 저장소 쪽에 LIMIT 로 제한과 삭제가 일어남이 놓이고 테스트 대역 쪽에 대본 목록 반환과 삭제 없음이 빗금으로 놓인다" caption="대역이 재현하지 못하는 것" zoom="false"
|
||||
:::
|
||||
|
||||
그 fake의 무제한 메서드는 미리 준 목록을 순서대로 반환하는 **대본**이고 아무것도 삭제하거나 제한하지 않는다. bounded 오버로드는 fake에도 있지만 job이 부르지 않아 실행되지 않는다.
|
||||
|
||||
## InboxOperationsTest 참조 위치
|
||||
|
||||
:::evidence key="messaging-inbox-jdbc-postgresql-f02" alt="코드베이스에서 InboxOperationsTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxOperationsTest 코드베이스 검색 — 1줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 통과가 증명하는 것과 이름이 주장하는 것
|
||||
|
||||
증명하는 것은 "0을 받을 때까지 루프를 돈다"이고 이름이 주장하는 "배치로 제한된다"가 아니다. 1000·500은 배치처럼 보이는 숫자다. **P1이 이 테스트를 통과한 채로 존재할 수 있었던 이유**다.
|
||||
|
||||
## 컨테이너 레인도 드러내지 못한다
|
||||
|
||||
`InboxPostgresIT.retentionRemovesOldRows`도 무제한 오버로드를 한 행에 대해 부른다. 후보는 fake의 무제한 메서드가 실제로 컬렉션에서 삭제하게 하고 bounded 메서드가 `limit`를 존중하게 하는 것 — 그러면 테스트가 P1을 잡는다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
테스트를 고쳐 결함 있는 구현과 올바른 구현을 가르는지 확인하지 않았다. 대역이 삭제도 제한도 하지 않는다는 코드로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-nats-experimental-f01
|
||||
title: deduplicatedPublish 를 무조건 참으로 선언하는데 실제 중복 제거는 프로파일에 창이 있을 때만 일어난다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-nats-experimental-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-nats-experimental-f01
|
||||
file: ../../../final/evidence/rendered/messaging-nats-experimental-f01.svg
|
||||
- key: messaging-nats-experimental-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-nats-experimental-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-nats-experimental-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-nats-experimental.md#L146 이다.
|
||||
module: messaging-nats-experimental
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# deduplicatedPublish 를 무조건 참으로 선언하는데 실제 중복 제거는 프로파일에 창이 있을 때만 일어난다
|
||||
|
||||
검증기의 capabilities() 도 같은 값을 돌려준다. 그런데 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어진다.
|
||||
|
||||
## 문제
|
||||
|
||||
검증기의 capabilities() 도 같은 값을 돌려준다.
|
||||
|
||||
그런데 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어진다.
|
||||
|
||||
## 결론
|
||||
|
||||
NatsJetStreamProfile.deduplicationWindow 는 Optional<Duration> 이고, 비어 있는 것이 정상 상태다 — 프로파일 생성자도 검증기도 창을 요구하지 않는다.
|
||||
|
||||
창이 없으면 Nats-Msg-Id 가 실리지 않고 서버는 중복을 제거하지 않는다.
|
||||
|
||||
즉 능력 선언이 프로파일과 무관하게 참이다.
|
||||
|
||||
이 저장소에서 능력 열두 개 중 부재가 예외를 만드는 유일한 것이 deduplicatedPublish 다(DefaultMessagePublisher:250).
|
||||
|
||||
나머지는 읽히지 않거나 분기에 쓰인다.
|
||||
|
||||
그러므로 이 플래그의 과대 선언은 다른 어느 플래그의 과대 선언보다 직접적이다 — 창 없는 목적지가 그 가드를 통과한다.
|
||||
|
||||
클래스 javadoc: "when the profile enables one" 이 정확히 능력이 담지 않은 조건이다.
|
||||
|
||||
창이 없는 목적지에서 모호를 재시도하면 스트림에 같은 메시지가 두 번 들어간다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : NatsJetStreamProfile 참조 17건 검색과 능력 상수 대비 중복 제거 식별자 생성 조건 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-nats-experimental.md#L146 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
능력은 상수이고 검증기의 `capabilities()` 도 같은 값을 돌려준다. 그런데 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어진다.
|
||||
|
||||
## 선언과 실제가 갈리는 조건
|
||||
|
||||
:::evidence key="messaging-nats-experimental-f01-diagram" alt="창 없는 프로파일이 능력은 참과 가드 통과를 지나 중복 저장으로 이어진다" caption="선언과 실제가 갈리는 조건" zoom="false"
|
||||
:::
|
||||
|
||||
`NatsJetStreamProfile.deduplicationWindow` 는 `Optional<Duration>` 이고, 비어 있는 것이 정상 상태다 — 프로파일 생성자도 검증기도 창을 요구하지 않는다. 창이 없으면 `Nats-Msg-Id` 가 실리지 않고 서버는 중복을 제거하지 않는다.
|
||||
|
||||
## NatsJetStreamProfile 참조 위치
|
||||
|
||||
:::evidence key="messaging-nats-experimental-f01" alt="코드베이스에서 NatsJetStreamProfile 를 검색한 출력 17줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="NatsJetStreamProfile 코드베이스 검색 — 17줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 이 플래그의 과대 선언이 가장 직접적이다
|
||||
|
||||
능력 열두 개 중 부재가 예외를 만드는 유일한 것이 `deduplicatedPublish` 다(`DefaultMessagePublisher:250`). 나머지는 읽히지 않거나 분기에 쓰인다. 그러므로 창 없는 목적지가 그 가드를 통과한다. 클래스 javadoc 의 "when the profile enables one" 이 정확히 능력이 담지 않은 조건이고, 창이 없는 목적지에서 모호를 재시도하면 스트림에 같은 메시지가 두 번 들어간다.
|
||||
|
||||
## 모순이 테스트로 고정되어 있다
|
||||
|
||||
`NatsAdapterContractTest` 안에서 같은 빈 창 프로파일(`confirming(Optional.empty())`)에 대해 둘 다 통과한다. 우연히 남은 것이 아니라는 뜻이고, 수정할 때 함께 고쳐야 할 지점이 어디인지도 이 두 개가 알려 준다.
|
||||
|
||||
## 수정
|
||||
|
||||
능력을 프로파일에서 파생시킨다. `NatsJetStreamProfile.durable(...)` 는 창을 2분으로 채워 주지만 호출자가 없고, 정규 생성자는 빈 창을 정상값으로 받는다(§4).
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
중복 제거 창이 없는 프로파일로 모호 재발행을 재현하지 않았다. 실제 JetStream 서버를 띄우지 않았다 — 클라이언트 브리지를 싣지 않는 리프다.
|
||||
|
||||
<!-- body:end -->
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-nats-experimental-f04
|
||||
title: 경과 시간 회귀를 막으려는 어셈블이 항상 참이다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-nats-experimental-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-nats-experimental-f04
|
||||
file: ../../../final/evidence/rendered/messaging-nats-experimental-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-nats-experimental-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-nats-experimental.md#L238 이다.
|
||||
module: messaging-nats-experimental
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 경과 시간 회귀를 막으려는 어셈블이 항상 참이다
|
||||
|
||||
as(...) 가 막으려는 회귀는 "모든 결과가 Duration.ZERO 를 보고하던 것"이다. 그런데 어셈블은 >= Duration.ZERO 다.
|
||||
|
||||
## 문제
|
||||
|
||||
as(...) 가 막으려는 회귀는 "모든 결과가 Duration.ZERO 를 보고하던 것"이다.
|
||||
|
||||
그런데 어셈블은 >= Duration.ZERO 다.
|
||||
|
||||
## 결론
|
||||
|
||||
Duration.ZERO 는 이 조건을 통과한다.
|
||||
|
||||
경과 시간은 시작 시점에서 잰 값이라 음수가 될 수 없으므로, 이 어셈블은 구현이 무엇을 하든 통과한다.
|
||||
|
||||
이름과 as 메시지가 정확히 짚은 회귀를, 어셈블만 못 잡는다.
|
||||
|
||||
그래서 이 테스트는 회귀 방지가 아니라 회귀 방지의 표시다.
|
||||
|
||||
isGreaterThan(Duration.ZERO) 로 바꾼다.
|
||||
|
||||
시간 분해능이 불안하면 전송 람다에 관측 가능한 지연을 넣고 그 하한과 비교한다 — 같은 클래스의 aPublishThatNeverCompletesIsBoundedByTheCallersTimeout 가 이미 50밀리초 마감으로 그 방식을 쓴다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : as 어셈블의 비교 연산자와 그것이 막겠다고 한 회귀 값의 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-nats-experimental.md#L238 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`as(...)` 가 막으려는 회귀는 "모든 결과가 `Duration.ZERO` 를 보고하던 것"이다. 그런데 어셈블은 `>= Duration.ZERO` 다 — `Duration.ZERO` 는 이 조건을 통과한다.
|
||||
|
||||
## as() 가 막으려던 회귀
|
||||
|
||||
:::evidence key="messaging-nats-experimental-f04" alt="분석 문서 analysis/messaging/messaging-nats-experimental.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-nats-experimental.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 구현이 무엇을 하든 통과한다
|
||||
|
||||
경과 시간은 시작 시점에서 잰 값이라 음수가 될 수 없다. 이름과 `as` 메시지가 정확히 짚은 회귀를 어셈블만 못 잡는다. 그래서 이 테스트는 회귀 방지가 아니라 회귀 방지의 표시다.
|
||||
|
||||
## 수정
|
||||
|
||||
`isGreaterThan(Duration.ZERO)` 로 바꾼다. 시간 분해능이 불안하면 전송 람다에 관측 가능한 지연을 넣고 그 하한과 비교한다 — 같은 클래스의 `aPublishThatNeverCompletesIsBoundedByTheCallersTimeout` 가 이미 50밀리초 마감으로 그 방식을 쓴다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
테스트를 실행하지 않았다. 항상 통과한다는 것은 어셈블 의미론으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-outbox-jdbc-postgresql-f02
|
||||
title: 배포되는 Debezium 설정이 수정 이전 버전이다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-outbox-jdbc-postgresql-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-outbox-jdbc-postgresql-f02
|
||||
file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-f02.svg
|
||||
- key: messaging-outbox-jdbc-postgresql-f02-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-outbox-jdbc-postgresql-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L888 이다.
|
||||
module: messaging-outbox-jdbc-postgresql
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 배포되는 Debezium 설정이 수정 이전 버전이다
|
||||
|
||||
src/main/resources/debezium/outbox-event-router.properties 가 event.key=destination 을 유지하고 있다. 같은 저장소의 Java(DebeziumOutboxEventRouter), V4 마이그레이션 주석, 그리고 전용 테스트(theRoutedKeyIsNotTheTopicName)가 모두 그것이 결함이라고 말한다 — "keying by destination puts every message on a topic onto one partition".
|
||||
|
||||
## 문제
|
||||
|
||||
src/main/resources/debezium/outbox-event-router.properties 가 event.key=destination 을 유지하고 있다.
|
||||
|
||||
같은 저장소의 Java(DebeziumOutboxEventRouter), V4 마이그레이션 주석, 그리고 전용 테스트(theRoutedKeyIsNotTheTopicName)가 모두 그것이 결함이라고 말한다 — "keying by destination puts every message on a topic onto one partition".
|
||||
|
||||
## 결론
|
||||
|
||||
추가로 헤더 매핑이 15개 중 4개뿐이라, 이 파일로 배포한 CDC 는 tenant·correlation·causation·producer·trace·partition/ordering key 를 전부 잃는다.
|
||||
|
||||
V4 가 존재하는 이유가 그 유실을 막는 것이다.
|
||||
|
||||
1.
|
||||
|
||||
properties 를 Java 설정에서 생성하거나, 최소한 둘을 대조하는 테스트를 둔다.
|
||||
|
||||
DebeziumOutboxEventRouter.connectorConfiguration("") 의 항목이 파일에 모두 있는지 확인하는 테스트면 충분하다.
|
||||
|
||||
지금은 두 표현을 잇는 코드가 한 줄도 없다.
|
||||
|
||||
2.
|
||||
|
||||
aggregateIdAsPartitionKey 를 connectorConfiguration 에 전달하거나, 전달할 수 없다면 DebeziumOutboxRecordMapper 에서 그 분기를 제거한다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : DebeziumOutboxEventRouter 참조 5건 검색과 배포 properties·Java 설정의 항목별 텍스트 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L888 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`src/main/resources/debezium/outbox-event-router.properties` 가 `event.key=destination` 을 유지하고 있다.
|
||||
|
||||
## 두 판본의 차이
|
||||
|
||||
:::evidence key="messaging-outbox-jdbc-postgresql-f02-diagram" alt="Java 설정 쪽에 aggregate id 키와 헤더 매핑 열다섯이 놓이고 배포 properties 쪽에 destination 키와 헤더 매핑 넷이 빗금으로 놓인다" caption="두 판본의 차이" zoom="false"
|
||||
:::
|
||||
|
||||
같은 저장소의 Java(`DebeziumOutboxEventRouter`), V4 마이그레이션 주석, 그리고 전용 테스트(`theRoutedKeyIsNotTheTopicName`)가 모두 그것이 결함이라고 말한다 — "keying by destination puts every message on a topic onto one partition".
|
||||
|
||||
## DebeziumOutboxEventRouter 참조 위치
|
||||
|
||||
:::evidence key="messaging-outbox-jdbc-postgresql-f02" alt="코드베이스에서 DebeziumOutboxEventRouter 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DebeziumOutboxEventRouter 코드베이스 검색 — 5줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 헤더 매핑도 4개뿐이다
|
||||
|
||||
이 파일로 배포한 CDC 는 tenant·correlation·causation·producer·trace·partition/ordering key 를 **전부 잃는다**. V4 가 존재하는 이유가 그 유실을 막는 것이다.
|
||||
|
||||
## 수정 둘
|
||||
|
||||
properties 를 Java 설정에서 생성하거나, 최소한 둘을 대조하는 테스트를 둔다 — `DebeziumOutboxEventRouter.connectorConfiguration("")` 의 항목이 파일에 모두 있는지 확인하는 테스트면 충분하다. 그리고 `aggregateIdAsPartitionKey` 를 `connectorConfiguration` 에 전달하거나, 전달할 수 없다면 `DebeziumOutboxRecordMapper` 에서 그 분기를 제거한다 — 지금은 모델이 커넥터가 하지 않을 일을 예측한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제 Debezium 커넥터를 띄워 properties 의 동작을 확인하지 않았다. 두 설정의 차이는 텍스트 대조로 확인했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-outbox-jdbc-postgresql-f03
|
||||
title: 역슬래시로 끝나는 헤더 값이 헤더 맵을 깨뜨린다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-outbox-jdbc-postgresql-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-outbox-jdbc-postgresql-f03
|
||||
file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-f03.svg
|
||||
- key: messaging-outbox-jdbc-postgresql-f03-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-outbox-jdbc-postgresql-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L899 이다.
|
||||
module: messaging-outbox-jdbc-postgresql
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 역슬래시로 끝나는 헤더 값이 헤더 맵을 깨뜨린다
|
||||
|
||||
findClosingQuote(:657-664)가 이스케이프된 역슬래시를 고려하지 않는다. 값이 역슬래시로 끝나면 파서가 종료 지점을 놓치고, 뒤에 헤더가 더 있으면 맵 전체가 붕괴한다(EVD-314, 런타임 재현).
|
||||
|
||||
## 문제
|
||||
|
||||
findClosingQuote(:657-664)가 이스케이프된 역슬래시를 고려하지 않는다.
|
||||
|
||||
값이 역슬래시로 끝나면 파서가 종료 지점을 놓치고, 뒤에 헤더가 더 있으면 맵 전체가 붕괴한다(EVD-314, 런타임 재현).
|
||||
|
||||
## 결론
|
||||
|
||||
HeaderValue 는 제어문자만 금지하므로 이 입력은 플랫폼 검증을 통과한다.
|
||||
|
||||
헤더 주입으로 이어지지는 않는다 — 예약 이름은 키가 아니라 값이 되고, 쓰기 경로가 예약 이름을 이미 거절한다.
|
||||
|
||||
수정: 종료 판정을 "앞의 연속된 역슬래시 개수가 짝수" 로 바꾸거나, 인덱스를 앞에서부터 스캔하며 이스케이프 상태를 추적한다.
|
||||
|
||||
후자가 unescape 와 대칭이라 낫다.
|
||||
|
||||
테스트는 OutboxPostgresIT.aHeaderValueWithControlCharactersRoundTrips 옆에 역슬래시 종결 케이스를 추가하면 된다 — 실 DB 왕복까지 확인할 수 있다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : HeaderValue 참조 26건 검색과 파서의 종료 지점 탐색 코드 확인. jshell 리플렉션으로 왕복 손상을 런타임 재현
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L899 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`findClosingQuote`(`:657-664`)가 이스케이프된 역슬래시를 고려하지 않는다.
|
||||
|
||||
## 구분자가 삼켜지는 지점
|
||||
|
||||
:::evidence key="messaging-outbox-jdbc-postgresql-f03-diagram" alt="역슬래시로 끝난 값이 종료 인용부 놓침과 뒤 헤더 흡수를 지나 헤더 맵 붕괴로 이어진다" caption="구분자가 삼켜지는 지점" zoom="false"
|
||||
:::
|
||||
|
||||
값이 역슬래시로 끝나면 파서가 종료 지점을 놓치고, 뒤에 헤더가 더 있으면 맵 전체가 붕괴한다(`EVD-314`, 런타임 재현).
|
||||
|
||||
## HeaderValue 참조 위치
|
||||
|
||||
:::evidence key="messaging-outbox-jdbc-postgresql-f03" alt="코드베이스에서 HeaderValue 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="HeaderValue 코드베이스 검색 — 26줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 플랫폼 검증을 통과한다
|
||||
|
||||
`HeaderValue` 는 제어문자만 금지한다.
|
||||
|
||||
## 헤더 주입으로 이어지지는 않는다
|
||||
|
||||
예약 이름은 키가 아니라 값이 되고, 쓰기 경로가 예약 이름을 이미 거절한다.
|
||||
|
||||
## 수정
|
||||
|
||||
종료 판정을 "앞의 연속된 역슬래시 개수가 짝수" 로 바꾸거나, 인덱스를 앞에서부터 스캔하며 이스케이프 상태를 추적한다 — 후자가 `unescape` 와 대칭이라 낫다. 테스트는 `OutboxPostgresIT.aHeaderValueWithControlCharactersRoundTrips` 옆에 역슬래시 종결 케이스를 추가하면 실 DB 왕복까지 확인할 수 있다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
없음 — 역슬래시 종결 값의 헤더 맵 붕괴를 런타임으로 재현했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-reliability-api-f01
|
||||
title: 한 인터페이스가 같은 전이의 두 세대를 갖고, 안전하지 않은 쪽에 @Deprecated가 없다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-reliability-api-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-reliability-api-f01
|
||||
file: ../../../final/evidence/rendered/messaging-reliability-api-f01.svg
|
||||
- key: messaging-reliability-api-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-reliability-api-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-reliability-api-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-reliability-api.md#L689 이다.
|
||||
module: messaging-reliability-api
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 한 인터페이스가 같은 전이의 두 세대를 갖고, 안전하지 않은 쪽에 @Deprecated가 없다
|
||||
|
||||
OutboxRepository가 다섯 전이 각각에 대해 MessageId 기반(반환 void)과 OutboxLease 기반(반환 OutboxTransitionResult) 두 형태를 선언한다. javadoc이 전자를 "deprecated for the relay's use"라고 부르지만 @Deprecated 애노테이션이 이 leaf 전체에 0건이다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **record의 `equals`를 좁히면 이유를 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
OutboxRepository가 다섯 전이 각각에 대해 MessageId 기반(반환 void)과 OutboxLease 기반(반환 OutboxTransitionResult) 두 형태를 선언한다.
|
||||
|
||||
javadoc이 전자를 "deprecated for the relay's use"라고 부르지만 @Deprecated 애노테이션이 이 leaf 전체에 0건이다.
|
||||
|
||||
## 결론
|
||||
|
||||
전자에는 fencing이 없다 — OutboxLease javadoc이 그 부재가 만든 이중 발행 사고를 기록한다.
|
||||
|
||||
컴파일러가 경고하지 않으므로 새 호출자가 그것을 고를 수 있고, 실제로 PostgreSQL 컨테이너 테스트가 그렇게 했다(§12.1c).
|
||||
|
||||
그리고 새 구현자는 17개 메서드를 전부 구현해야 하며 그중 다섯은 안전하지 않은 형태다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : OutboxRepository 참조 35건 검색과 다섯 전이의 두 세대 시그니처 및 @Deprecated 수 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-reliability-api.md#L689 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`OutboxRepository`가 다섯 전이 각각에 대해 `MessageId` 기반(반환 `void`)과 `OutboxLease` 기반(반환 `OutboxTransitionResult`) 두 형태를 선언한다.
|
||||
|
||||
## 표시가 없는 두 형태
|
||||
|
||||
:::evidence key="messaging-reliability-api-f01-diagram" alt="lease 기반 쪽에 결과 반환과 fencing 있음이 놓이고 MessageId 기반 쪽에 void 반환과 fencing 없음이 빗금으로 놓인다" caption="표시가 없는 두 형태" zoom="false"
|
||||
:::
|
||||
|
||||
javadoc이 전자를 "deprecated for the relay's use"라고 부르지만 `@Deprecated` 애노테이션이 이 leaf 전체에 **0건**이다.
|
||||
|
||||
## OutboxRepository 참조 위치
|
||||
|
||||
:::evidence key="messaging-reliability-api-f01" alt="코드베이스에서 OutboxRepository 를 검색한 출력 35줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxRepository 코드베이스 검색 — 35줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 컴파일러가 경고하지 않는다
|
||||
|
||||
전자에는 fencing이 없다 — `OutboxLease` javadoc이 그 부재가 만든 이중 발행 사고를 기록한다. 새 호출자가 그것을 고를 수 있고, **실제로 PostgreSQL 컨테이너 테스트가 그렇게 했다**(§12.1c). 그리고 새 구현자는 17개 메서드를 전부 구현해야 하며 그중 다섯은 안전하지 않은 형태다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
구세대를 쓰는 배포를 만들어 fencing 부재의 결과를 관측하지 않았다. 이 저장소에는 그런 배포가 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-spring-boot-starter-f05
|
||||
title: 배치 발행자가 CompletionStage 를 돌려주면서 동기 예외를 던진다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-spring-boot-starter-f05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-spring-boot-starter-f05
|
||||
file: ../../../final/evidence/rendered/messaging-spring-boot-starter-f05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-spring-boot-starter-f05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-spring-boot-starter.md#L367 이다.
|
||||
module: messaging-spring-boot-starter
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 배치 발행자가 CompletionStage 를 돌려주면서 동기 예외를 던진다
|
||||
|
||||
같은 클래스가 자기 의존 대상에 대해서는 정확히 이 형태를 방어한다. 즉 "게으르게 검증하고 실패한 스테이지를 돌려준다" 가 이 클래스가 아는 계약인데, 자기 호출자에게는 그것을 지키지 않는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **검증기는 발행이 아니라 주입이 강제다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
같은 클래스가 자기 의존 대상에 대해서는 정확히 이 형태를 방어한다.
|
||||
|
||||
즉 "게으르게 검증하고 실패한 스테이지를 돌려준다" 가 이 클래스가 아는 계약인데, 자기 호출자에게는 그것을 지키지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
비동기 파이프라인으로 배치를 부르는 코드는 .exceptionally(...) 로 잡히지 않는 예외를 만난다.
|
||||
|
||||
등급이 P3 인 이유는 이것이 프로그래밍 오류(배치 크기 초과)이고 결과가 손실이 아니라 예외 형태의 불일치이기 때문이다.
|
||||
|
||||
전용 테스트(aBatchLargerThanItsLimitIsRefusedBeforeAnythingIsPublished)가 assertThatThrownBy 로 현재 동작을 고정하고 있으므로, 고치려면 그 테스트도 함께 바꾼다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 반환 타입과 예외 전달 경로 대조, 같은 클래스가 의존 대상에 적용한 방어와의 비교
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-spring-boot-starter.md#L367 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
배치 발행자가 `CompletionStage` 를 돌려주면서 동기 예외를 던진다.
|
||||
|
||||
## 반환 타입과 예외 전달 방식
|
||||
|
||||
:::evidence key="messaging-spring-boot-starter-f05" alt="분석 문서 analysis/messaging/messaging-spring-boot-starter.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-spring-boot-starter.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 같은 클래스가 자기 의존 대상에는 이 형태를 방어한다
|
||||
|
||||
"게으르게 검증하고 실패한 스테이지를 돌려준다" 가 이 클래스가 아는 계약인데, 자기 호출자에게는 그것을 지키지 않는다.
|
||||
|
||||
## 비동기 파이프라인이 잡지 못한다
|
||||
|
||||
`.exceptionally(...)` 로 잡히지 않는 예외를 만난다. 등급이 P3 인 이유는 이것이 프로그래밍 오류(배치 크기 초과)이고 결과가 손실이 아니라 예외 형태의 불일치이기 때문이다.
|
||||
|
||||
## 테스트가 현재 동작을 고정하고 있다
|
||||
|
||||
`aBatchLargerThanItsLimitIsRefusedBeforeAnythingIsPublished` 가 `assertThatThrownBy` 를 쓰므로, 고치려면 그 테스트도 함께 바꾼다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
@ConditionalOnBean 의 평가 순서를 실제 컨텍스트로 재현하지 않았다. 스프링의 문서화된 제약으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-testkit-f07
|
||||
title: 항등식을 단언하는 테스트가 하나 있다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-testkit-f07
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-testkit-f07
|
||||
file: ../../../final/evidence/rendered/messaging-testkit-f07.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-testkit-f07.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-testkit.md#L980 이다.
|
||||
module: messaging-testkit
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 항등식을 단언하는 테스트가 하나 있다
|
||||
|
||||
CompatibilityMatrixTest.aCertificationClaimCannotBeMadeWithoutEvidence(:48-60)의 좌변과 우변이 같은 식이다(§12.3(c)). 이름이 약속하는 것을 검사하지 않는다.
|
||||
|
||||
## 문제
|
||||
|
||||
CompatibilityMatrixTest.aCertificationClaimCannotBeMadeWithoutEvidence(:48-60)의 좌변과 우변이 같은 식이다(§12.3(c)).
|
||||
|
||||
이름이 약속하는 것을 검사하지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
실질 검사는 같은 파일의 다른 두 테스트가 하고 있으므로 커버리지 손실은 없다.
|
||||
|
||||
이 테스트를 지우거나, "증거를 비우면 Stable 주장이 무너진다" 를 실제로 검사하도록 바꾼다 — 후자가 이름에 맞는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : CompatibilityMatrixTest 참조 검색과 해당 단언의 좌변·우변 식 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-testkit.md#L980 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`CompatibilityMatrixTest.aCertificationClaimCannotBeMadeWithoutEvidence`(`:48-60`)의 좌변과 우변이 같은 식이다(§12.3(c)). 이름이 약속하는 것을 검사하지 않는다.
|
||||
|
||||
## CompatibilityMatrixTest 참조 위치
|
||||
|
||||
:::evidence key="messaging-testkit-f07" alt="코드베이스에서 CompatibilityMatrixTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CompatibilityMatrixTest 코드베이스 검색 — 1줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 커버리지 손실은 없다
|
||||
|
||||
실질 검사는 같은 파일의 다른 두 테스트가 하고 있다. 이 테스트를 지우거나, "증거를 비우면 Stable 주장이 무너진다" 를 실제로 검사하도록 바꾼다 — 후자가 이름에 맞는다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
테스트를 실행하지 않았다. 좌변과 우변이 같은 식이라는 것을 본문으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-transport-spi-f01
|
||||
title: 드레인 마감 30초가 세 곳에서 독립적으로 결정된다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-transport-spi-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-transport-spi-f01
|
||||
file: ../../../final/evidence/rendered/messaging-transport-spi-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-transport-spi-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-transport-spi.md#L659 이다.
|
||||
module: messaging-transport-spi
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 드레인 마감 30초가 세 곳에서 독립적으로 결정된다
|
||||
|
||||
MessagingLifecycle.DEFAULT_DRAIN_DEADLINE(public), DefaultMessagingRuntimeRegistry.DEFAULT_DRAIN_DEADLINE(private), MessagingShutdownLifecycle의 생성자 인자. public 상수가 같은 leaf 안에 있는데 다른 클래스가 자기 private 복사본을 쓴다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **같은 개념의 sentinel은 계층을 넘어 하나로 정한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
MessagingLifecycle.DEFAULT_DRAIN_DEADLINE(public), DefaultMessagingRuntimeRegistry.DEFAULT_DRAIN_DEADLINE(private), MessagingShutdownLifecycle의 생성자 인자.
|
||||
|
||||
public 상수가 같은 leaf 안에 있는데 다른 클래스가 자기 private 복사본을 쓴다.
|
||||
|
||||
## 결론
|
||||
|
||||
MessagingLifecycleTest.theDefaultDrainDeadlineMatchesTheDesign이 public 쪽만 고정하므로 private 쪽이 바뀌어도 통과한다.
|
||||
|
||||
그리고 §17 첫 항목대로 public 상수가 있는 인터페이스는 구현체가 없다 — 즉 살아 있는 값(private)이 죽은 인터페이스의 값(public)을 참조하지 않는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : MessagingShutdownLifecycle 참조 11건 검색과 같은 값이 독립적으로 선언된 세 지점 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-transport-spi.md#L659 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`MessagingLifecycle.DEFAULT_DRAIN_DEADLINE`(public), `DefaultMessagingRuntimeRegistry.DEFAULT_DRAIN_DEADLINE`(private), `MessagingShutdownLifecycle`의 생성자 인자 — 30초가 세 곳에서 독립적으로 결정된다.
|
||||
|
||||
## MessagingShutdownLifecycle 참조 위치
|
||||
|
||||
:::evidence key="messaging-transport-spi-f01" alt="코드베이스에서 MessagingShutdownLifecycle 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingShutdownLifecycle 코드베이스 검색 — 11줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 테스트가 한쪽만 고정한다
|
||||
|
||||
`MessagingLifecycleTest.theDefaultDrainDeadlineMatchesTheDesign`이 public 쪽만 고정하므로 private 쪽이 바뀌어도 통과한다.
|
||||
|
||||
## 살아 있는 값이 죽은 인터페이스를 참조하지 않는다
|
||||
|
||||
§17 첫 항목대로 public 상수가 있는 인터페이스는 구현체가 없다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
세 값이 실제 배포에서 어긋난 적이 있는지 확인하지 않았다. 오늘은 같은 값이고, 잇는 코드가 없다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-cloudevents-f03
|
||||
title: 왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-cloudevents-f03
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-cloudevents.md#L538
|
||||
---
|
||||
|
||||
# 왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다
|
||||
|
||||
## 관계
|
||||
|
||||
- **배포 아티팩트가 싣지만 아무도 부르지 않는다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **상호운용을 위한 매퍼가 명세 준수 이벤트를 분류되지 않은 예외로 거절한다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
이름이 왕복이라고 말하는 테스트가 실제로는 부분 보존만 확인할 때, 읽는 사람은 확인되지 않은 필드를 확인된 것으로 읽는다. 그 착각이 가장 비싸게 끝나는 필드가 trace 다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 비교하는 필드와 왕복하는 필드를 각각 센다
|
||||
fromCloudEvent 가 partitionKey 와 orderingKey 를 empty 로, traceContext 를 none() 으로, headers 를 empty() 로 두고 producedAt 을 occurredAt 값으로 덮는다. 테스트는 여섯 필드만 비교하고 이 다섯을 비교하지 않는다.
|
||||
|
||||
2. 비교하지 않는 필드가 실제로 어긋나는지 확인한다
|
||||
fixture 의 producedAt 은 09:15:01Z, occurredAt 은 09:15:00Z 다. 비교했다면 실패했을 값이다.
|
||||
|
||||
3. 소실을 계약에 적거나 테스트 이름을 실제 보장에 맞춘다
|
||||
messaging-core-api 의 TraceContext javadoc 이 그 필드를 봉투에 둔 이유를 "a trace survives an Outbox round trip through the database, where broker headers do not exist yet" 라고 적는다. CloudEvents 왕복이 그 보존을 깨뜨리면서, CloudEvents 자신이 정의하는 distributed-tracing extension 도 쓰지 않는다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
매핑이 양방향이고 한쪽 방향에서 필드가 줄어드는 모든 코덱·어댑터 경계.
|
||||
|
||||
## 예외
|
||||
|
||||
SSOT 가 이 규칙의 반례를 적지 않았다. 의도적으로 버리는 필드가 있다면 그 목록이 javadoc 에 있어야 한다는 것이 여기서 제시된 후보 중 하나다.
|
||||
|
||||
## 예시
|
||||
|
||||
DefaultCloudEventMapper.java:115-131 의 매핑과 CloudEventMappingTest.java:72-84, 143-161 의 비교 대상.
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-kafka-share-experimental-f05
|
||||
title: leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-kafka-share-experimental-f05
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-kafka-share-experimental.md#L503
|
||||
---
|
||||
|
||||
# leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다
|
||||
|
||||
## 관계
|
||||
|
||||
- **"등록"이 아무것도 등록하지 않고 성공을 반환한다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
네 타입 중 KafkaShareProfileValidator 만 테스트를 갖는다. registrar 에 테스트가 있었다면 register 가 spec 을 버리는 것이 sink 호출 확인에서 바로 드러났을 것이고, capability 의 boolean 12개는 지금 순서 값을 true 로 바꿔도 아무것도 깨지지 않는다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 테스트 클래스 이름 목록과 public 타입 목록을 맞춘다
|
||||
find src/test -name '*Test.java' 가 하나를 돌려준다.
|
||||
|
||||
2. 짝이 없는 타입이 무엇을 혼자 결정하는지 센다
|
||||
registrar 는 register 의 spec 처리를, capability 는 12개 boolean 선언을 혼자 갖는다.
|
||||
|
||||
3. 선언만 있는 상수도 테스트 대상이다
|
||||
읽는 코드가 없더라도 값이 바뀌면 안 되는 것이면 그 사실을 테스트가 붙든다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
타입 수가 적어 테스트 하나로 충분해 보이는 leaf. 특히 미배선 상태라 실행 경로가 없는 leaf.
|
||||
|
||||
## 예외
|
||||
|
||||
테스트가 소비자 leaf 에 있는 경우는 예외로 볼 수 있다. 이 leaf 는 소비자가 0 이라 그 예외에 해당하지 않는다.
|
||||
|
||||
## 예시
|
||||
|
||||
테스트 클래스 목록이 하나라는 것과, 그 하나가 validator 를 겨냥한다는 것.
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-reliability-api-f04
|
||||
title: 계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-reliability-api-f04
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-reliability-api.md#L716
|
||||
---
|
||||
|
||||
# 계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다
|
||||
|
||||
## 관계
|
||||
|
||||
- **inbox 보존 규칙이 문서로만 있다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **포트가 bounded/unbounded purge 두 오버로드를 나란히 노출하고, 호출자가 무제한 쪽을 고른다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **fencing token 경로가 실제 데이터베이스에 대해 실행되지 않는다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **한 인터페이스가 같은 전이의 두 세대를 갖고, 안전하지 않은 쪽에 `@Deprecated`가 없다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
계약 불변식 중 일부는 구현이 우연히 그 조합을 만들지 않으면 한 번도 실행되지 않는다. OutboxCanonicalMetadata 가 schemaUri 있고 schemaSubject 없는 조합을 거절하는 것, OutboxLease 가 token < 1 을 거절하는 것, InboxResult.isSafeToSettle 의 세 값이 그런 것들이다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. leaf 에 src/test 가 있는지부터 본다
|
||||
messaging-reliability-api 에는 그 디렉터리가 없다. 13개 타입의 record 생성자 검증 여섯과 술어 셋이 이 leaf 의 레인에서 실행되지 않는다.
|
||||
|
||||
2. 형제 leaf 의 기준선을 확인한다
|
||||
messaging-core-api 79개, messaging-policy 42개다. 계약 leaf 라서 테스트가 없는 것이 아니다.
|
||||
|
||||
3. 거절 조건과 술어를 겨냥한 단위 테스트를 둔다
|
||||
구현이 지나가는 경로가 아니라 계약이 금지하는 조합을 겨냥한다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
타입 선언과 javadoc 만 담는 계약 leaf. 특히 record 생성자가 검증을 갖는 leaf.
|
||||
|
||||
## 예외
|
||||
|
||||
SSOT 가 이 규칙의 반례를 적지 않았다. 검증이 전혀 없는 순수 인터페이스 leaf 라면 대상이 아니지만, 이 leaf 는 생성자 검증 여섯을 갖는다.
|
||||
|
||||
## 예시
|
||||
|
||||
ls src/messaging/messaging-reliability-api/src 가 main 만 돌려준다는 것. 원문 근거는 evidence/raw/289 §A 이다.
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-reliability-api-f07
|
||||
title: record의 equals를 좁히면 이유를 적는다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-reliability-api-f07
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-reliability-api.md#L743
|
||||
---
|
||||
|
||||
# record의 equals를 좁히면 이유를 적는다
|
||||
|
||||
## 관계
|
||||
|
||||
- **inbox 보존 규칙이 문서로만 있다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **포트가 bounded/unbounded purge 두 오버로드를 나란히 노출하고, 호출자가 무제한 쪽을 고른다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **fencing token 경로가 실제 데이터베이스에 대해 실행되지 않는다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **한 인터페이스가 같은 전이의 두 세대를 갖고, 안전하지 않은 쪽에 `@Deprecated`가 없다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
같은 messageId · status · attempts · payload 를 가진 두 행이 다른 목적지, 다른 provenance 를 가져도 같다고 판정된다. 컬렉션 연산과 테스트 단언에서 의미가 달라지는데, 그 좁힘의 이유가 어디에도 없다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 어떤 필드를 비교에서 뺐는지 센다
|
||||
OutboxRecord.equals 와 hashCode 가 넷만 본다. destination · metadata · createdAt · leaseExpiresAt · lastFailureCode 는 무시한다.
|
||||
|
||||
2. 재정의가 필요했던 이유와 좁힌 이유를 구분한다
|
||||
배열 필드 때문에 재정의가 필요한 것까지는 명확하다. messaging-schema-api 의 EncodedMessage 도 같은 이유로 재정의한다.
|
||||
|
||||
3. 같은 이유에서 출발한 형제와 대조한다
|
||||
EncodedMessage 는 모든 필드를 비교한다. 여기서 갈라진 지점이 기록돼야 할 자리다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
배열 필드나 파생 필드 때문에 record 의 기본 equals 를 재정의하는 모든 자리.
|
||||
|
||||
## 예외
|
||||
|
||||
SSOT 가 이 규칙의 반례를 적지 않았다. 신원 필드만으로 동등성을 정의하는 설계가 의도라면 그 문장이 javadoc 에 있어야 한다.
|
||||
|
||||
## 예시
|
||||
|
||||
OutboxRecord.java:114-126 의 비교 대상. 확인 방법은 그것과 EncodedMessage 의 equals 를 대조하는 것이다.
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-runtime-core-f06
|
||||
title: 증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-runtime-core-f06
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-runtime-core.md#L754
|
||||
---
|
||||
|
||||
# 증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다
|
||||
|
||||
## 관계
|
||||
|
||||
- **관측이 구현·호출부·주입 자리를 모두 갖추고도 출하에서 no-op이다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **소비 오케스트레이터가 조립되지 않는다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **선언된 content type과 실제 인코딩이 조용히 갈라질 수 있다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
진단에서 generation 을 읽는 사람은 언제나 1 을 본다. 회전 코드가 없는 지금은 무해하지만, DefaultMessagingRuntimeRegistry 의 세대 드레인 로직은 세대 구분을 전제한다. 회전을 붙일 때 이 리터럴이 잊히면 두 세대가 같은 번호를 갖는다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 증가한다고 적힌 값의 설치 지점을 찾는다
|
||||
유일한 지점인 MessagingCoreAutoConfiguration:476 이 리터럴 1L 을 넘긴다.
|
||||
|
||||
2. 문서가 무엇을 약속하는지 확인한다
|
||||
MessagingRuntime.generation() javadoc 은 "increasing with each replacement", TransportMessagingRuntime javadoc 은 "the credential generation a rotation increments" 라고 적는다.
|
||||
|
||||
3. 값을 실제 카운터에 연결하거나 리터럴임을 주석으로 남긴다
|
||||
둘 중 하나가 없으면 문서와 값이 조용히 갈라진 상태로 남는다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
javadoc 이 값의 변화를 약속하고 그 값의 생성 지점이 조립 코드에 하나뿐인 자리.
|
||||
|
||||
## 예외
|
||||
|
||||
SSOT 가 이 규칙의 반례를 적지 않았다. 값이 영원히 고정이라면 그것이 문서의 표현이어야 하고, 지금은 문서 쪽이 증가를 말한다.
|
||||
|
||||
## 예시
|
||||
|
||||
MessagingCoreAutoConfiguration:476 의 리터럴과 두 javadoc. 확인 방법은 git grep -n 'TransportMessagingRuntime(' -- src/main 이다.
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-schema-avro-f04
|
||||
title: 모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-schema-avro-f04
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-schema-avro.md#L551
|
||||
---
|
||||
|
||||
# 모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다
|
||||
|
||||
## 관계
|
||||
|
||||
- **진화 판단이 두 곳에 있고 형태가 반대다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **CI에서 돈다고 선언한 게이트를 부르는 CI가 없다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
transitive 모드는 "여러 릴리스 뒤처진 consumer" 를 위한 것이고 그것이 이 게이트가 존재하는 이유의 절반이다. 그 절반이 한 번도 실행되지 않는다. 그리고 그 분기가 §12.3 의 중복 구현이 갈라질 지점이기도 하다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 분기 조건이 되는 enum 값과 테스트가 넘긴 값을 맞춰 본다
|
||||
AvroCompatibilityTest 의 게이트 호출 2건이 모두 SchemaCompatibility.BACKWARD 다. isTransitive 가 true 인 경로가 실행되지 않는다.
|
||||
|
||||
2. 실행되지 않는 분기가 무엇을 결정하는지 센다
|
||||
여기서는 비교 대상 스키마의 개수와 범위가 통째로 달라진다.
|
||||
|
||||
3. 분기마다 입력을 만든다
|
||||
v1 · v2 · v3 세 스키마로 BACKWARD_TRANSITIVE 케이스를 추가한다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
enum 이 알고리즘을 가르는 모든 게이트·검증기·전략 선택 지점.
|
||||
|
||||
## 예외
|
||||
|
||||
SSOT 가 이 규칙의 반례를 적지 않았다. 분기가 같은 코드로 수렴하는 것이 증명돼 있다면 대상이 아니지만, 여기서는 두 분기가 다른 비교를 수행한다.
|
||||
|
||||
## 예시
|
||||
|
||||
AvroCompatibilityTest.java:134-150 의 모드 인자 두 개. 확인 방법은 두 테스트의 인자를 보는 것이다.
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-schema-protobuf-f01
|
||||
title: 검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-schema-protobuf-f01
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-schema-protobuf.md#L527
|
||||
---
|
||||
|
||||
# 검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다
|
||||
|
||||
## 관계
|
||||
|
||||
- **registry 조회 로직이 세 codec에 복제돼 있다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
테스트 javadoc 이 .proto 를 "the fixture documents" 라고 부르므로 읽는 사람은 그 파일이 테스트의 근거라고 믿는다. 실제로는 테스트가 그 파일을 읽지 않고 DescriptorProto 로 같은 스키마를 손수 만든다. 한쪽만 수정되면 조용히 갈라진다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 스키마 파일이 빌드나 테스트에 입력으로 들어가는지 확인한다
|
||||
src/test/proto/order_created_v1.proto 는 어디에도 입력되지 않는다. 오늘 두 정의는 일치한다 — 필드 4개, 태그 1–4, 타입까지 전수 대조했다.
|
||||
|
||||
2. 검증되지 않는다면 그 사실을 파일 안에 적는다
|
||||
"이 파일은 문서이며 테스트는 descriptor 를 손수 만든다" 가 그 문장이다.
|
||||
|
||||
3. 자동 대조가 가능한지 먼저 따져 본다
|
||||
protoc 없이 protobuf-java 파서만으로는 .proto 를 읽어 descriptor 를 만들 수 없다. protoc 를 뺀 것은 이유가 적힌 설계 결정이다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
빌드 입력이 아닌 스키마·설정 예제 파일이 저장소에 남아 있는 자리.
|
||||
|
||||
## 예외
|
||||
|
||||
빌드가 그 파일을 실제로 소비하면 이 규칙의 대상이 아니다. 여기서는 소비 지점이 없다.
|
||||
|
||||
## 예시
|
||||
|
||||
.proto 전문과 ProtobufCompatibilityTest.java:41-66 의 손수 만든 descriptor. 확인 방법은 두 정의의 필드·태그·타입 대조와 find src/messaging/messaging-schema-protobuf -name '*.proto' 다.
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-schema-protobuf-f02
|
||||
title: 신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-schema-protobuf-f02
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-schema-protobuf.md#L536
|
||||
---
|
||||
|
||||
# 신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다
|
||||
|
||||
## 관계
|
||||
|
||||
- **registry 조회 로직이 세 codec에 복제돼 있다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
디코딩은 브로커에서 온 바이트를 받는 쪽이다. 지금은 자기 코드가 만든 바이트에 대한 방어가 남이 만든 바이트에 대한 방어보다 잘 검증돼 있다. 형제 leaf 는 반대로 되어 있다 — AvroRegistryBoundsTest.theEvolutionDecodeAppliesTheSameBound 가 정확히 이 각도를 덮는다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 상한 검사가 인코딩 쪽과 디코딩 쪽에 각각 있는지 본다
|
||||
ProtobufMessageCodec.java:104-108 의 if (encoded.length > maxBytes) 가 디코딩 쪽 검사다.
|
||||
|
||||
2. 각 검사에 테스트가 붙었는지 센다
|
||||
인코딩 상한은 두 테스트가 덮고, 디코딩 분기를 겨냥한 테스트는 ProtobufCompatibilityTest 12개 중 없다.
|
||||
|
||||
3. 입력의 출처로 우선순위를 정한다
|
||||
maxBytes 보다 큰 byte[] 로 decode 를 부르는 테스트를 먼저 추가한다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
같은 상한이 인코딩과 디코딩 양쪽에 걸려 있고 한쪽 입력만 외부에서 오는 모든 codec.
|
||||
|
||||
## 예외
|
||||
|
||||
디코딩 입력이 같은 프로세스에서 만들어진다고 타입이 보장하면 대상이 아니다. 이 codec 의 디코딩 입력은 브로커에서 온다.
|
||||
|
||||
## 예시
|
||||
|
||||
ProtobufMessageCodec.java:104-108 의 분기와 ProtobufCompatibilityTest 12개 전수. 확인 방법은 그 12개 중 decode 에 큰 입력을 주는 것이 없음을 보는 것이다.
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-security-f05
|
||||
title: 같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다
|
||||
topic: verification-path-coverage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-security-f05
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-security.md#L670
|
||||
---
|
||||
|
||||
# 같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다
|
||||
|
||||
## 관계
|
||||
|
||||
- **종료 시 자격증명 소거가 호출되지 않는다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **같은 TLS posture를 두 클래스가 다른 엄격도로 검사한다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **권한 거부가 `AUTHORIZATION`이 아니라 `CONFIGURATION`으로 기록된다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
테스트가 고정하는 것과 실행되는 것이 다른 객체다. 한쪽만 고치면 다른 쪽은 조용히 다른 시점에 회전한다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 같은 판단을 하는 메서드가 둘인지 센다
|
||||
CredentialRotationPlan.isDue 와 isExpired, CredentialRuntime.isDueForRotation 과 isExpired 가 글자까지 같다.
|
||||
|
||||
2. 실행되는 쪽과 테스트되는 쪽이 같은지 본다
|
||||
전자는 소비자가 0 이고 전용 테스트 CredentialRotationContractTest 4개를 갖는다. 후자가 실행되는 쪽이다.
|
||||
|
||||
3. 위임으로 하나를 만든다
|
||||
CredentialRuntime 이 CredentialRotationPlan 을 필드로 갖고 위임하거나, 계획 record 를 제거한다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
같은 시간·상태 판단이 값 객체와 런타임 객체에 각각 구현되는 자리.
|
||||
|
||||
## 예외
|
||||
|
||||
SSOT 가 이 규칙의 반례를 적지 않았다. 두 술어가 의도적으로 다른 기준을 갖는다면 그 차이가 이름이나 javadoc 에 있어야 하는데, 지금은 본문이 같다.
|
||||
|
||||
## 예시
|
||||
|
||||
두 메서드의 본문. 원문 근거는 evidence/raw/287 §E 이고, 확인 방법은 두 본문을 대조하는 것이다.
|
||||
|
||||
Reference in New Issue
Block a user