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
+95
@@ -0,0 +1,95 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-advanced-diagnostics-f03
|
||||
title: "실환경 증거" 가 두 리프에 반씩 있고 서로 만나지 않는다
|
||||
topic: contract-domain-and-bounds
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-advanced-diagnostics-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-diagnostics-f03
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-diagnostics-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-diagnostics-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-diagnostics.md#L218 이다.
|
||||
module: grpc-advanced-diagnostics
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# "실환경 증거" 가 두 리프에 반씩 있고 서로 만나지 않는다
|
||||
|
||||
이 리프가 능력별로 무엇이 실환경인지 정의한다. 그리고 grpc-advanced-bootstrap 이 승격 증거로 그것을 요구한다.
|
||||
|
||||
## 문제
|
||||
|
||||
이 리프가 능력별로 무엇이 실환경인지 정의한다.
|
||||
|
||||
그리고 grpc-advanced-bootstrap 이 승격 증거로 그것을 요구한다.
|
||||
|
||||
## 결론
|
||||
|
||||
GrpcAdvancedPromotionGate.evaluate 는 그 불리언이 거짓이면 "xds has no real environment test" 를 차단 사유로 낸다.
|
||||
|
||||
그 불리언을 무엇으로 채워야 하는지는 그쪽에서 답하지 않고, 답하는 코드가 이 리프에 있는데 두 쪽이 서로를 부르지 않는다.
|
||||
|
||||
결과: GrpcAdvancedPromotionEvidence.complete(XDS, 7일) 은 realEnvironmentTest = true 를 그냥 넣는다.
|
||||
|
||||
xDS 통제 평면이 실제로 있었는지와 무관하다.
|
||||
|
||||
이 리프의 javadoc 이 경계한 상태 — "a suite that runs without the infrastructure passes and establishes nothing" — 를 승격 게이트가 그대로 통과시킬 수 있다.
|
||||
|
||||
두 리프 모두 배선되지 않았고 승격은 사람이 수행한다.
|
||||
|
||||
다만 이 두 조각이 존재하는 이유가 "그 판단을 코드로 적어 두는 것" 이므로, 판단의 절반이 다른 절반을 부르지 않는 것은 그 목적에 어긋난다.
|
||||
|
||||
grpc-advanced-edition §17.2 가 같은 가족에서 같은 모양을 기록했다 — 두 승격 게이트가 서로를 부르지 않는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcAdvancedPromotionGate 참조 14건 검색과 두 리프의 실환경 증거 정의 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-advanced-diagnostics.md#L218 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
이 리프가 능력별로 무엇이 실환경인지 정의한다.
|
||||
|
||||
```java
|
||||
public static Set<Infrastructure> requiredFor(GrpcAdvancedCapability capability) { … }
|
||||
public static List<String> missingInfrastructure(GrpcAdvancedCapability capability, Set<Infrastructure> available) { … }
|
||||
```
|
||||
|
||||
그리고 `grpc-advanced-bootstrap` 이 승격 증거로 그것을 요구하는데, 증거는 불리언 하나다.
|
||||
|
||||
## GrpcAdvancedPromotionGate 참조 위치
|
||||
|
||||
:::evidence key="grpc-advanced-diagnostics-f03" alt="코드베이스에서 GrpcAdvancedPromotionGate 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcAdvancedPromotionGate 코드베이스 검색 — 14줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 두 쪽이 서로를 부르지 않는다
|
||||
|
||||
`GrpcAdvancedPromotionGate.evaluate` 는 그 불리언이 거짓이면 "xds has no real environment test" 를 차단 사유로 낸다. 그 불리언을 무엇으로 채워야 하는지는 그쪽에서 답하지 않고, 답하는 코드가 이 리프에 있다. 결과적으로 `GrpcAdvancedPromotionEvidence.complete(XDS, 7일)` 은 `realEnvironmentTest = true` 를 그냥 넣는다 — xDS 통제 평면이 실제로 있었는지와 무관하게. 이 리프의 javadoc 이 경계한 상태("a suite that runs without the infrastructure passes and establishes nothing")를 승격 게이트가 그대로 통과시킬 수 있다.
|
||||
|
||||
## 왜 P3 인가
|
||||
|
||||
두 리프 모두 배선되지 않았고 승격은 사람이 수행한다. 다만 이 두 조각이 존재하는 이유가 "그 판단을 코드로 적어 두는 것" 이므로, 판단의 절반이 다른 절반을 부르지 않는 것은 그 목적에 어긋난다. `grpc-advanced-edition` §17.2 가 같은 가족에서 같은 모양을 기록했다.
|
||||
|
||||
## 수정
|
||||
|
||||
`GrpcAdvancedPromotionEvidence.realEnvironmentTest` 를 불리언 대신 `Set<Infrastructure> availableInfrastructure` 로 바꾸고, 게이트가 `missingInfrastructure(capability, available)` 를 불러 그 결과를 차단 사유에 합친다. 그러면 "실환경 테스트를 했다" 가 선언이 아니라 능력별 목록에 대한 대조가 된다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
두 리프를 실제로 이어 승격 증거가 흐르는지 확인하지 않았다. 각 리프가 선언한 항목의 대조로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-advanced-resilience-f02
|
||||
title: 대체 선택기는 사용자 정의 선택기가 받는 보호를 받지 않는다
|
||||
topic: contract-domain-and-bounds
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-advanced-resilience-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-resilience-f02
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-resilience-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-resilience-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-resilience.md#L155 이다.
|
||||
module: grpc-advanced-resilience
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 대체 선택기는 사용자 정의 선택기가 받는 보호를 받지 않는다
|
||||
|
||||
fallback.pick(selectable) 은 감싸이지 않는다. 대체가 던지면 예외가 그대로 올라가고, 널이나 목록 밖 엔드포인트를 돌려주면 그대로 결정이 된다.
|
||||
|
||||
## 문제
|
||||
|
||||
fallback.pick(selectable) 은 감싸이지 않는다.
|
||||
|
||||
대체가 던지면 예외가 그대로 올라가고, 널이나 목록 밖 엔드포인트를 돌려주면 그대로 결정이 된다.
|
||||
|
||||
## 결론
|
||||
|
||||
기본 생성자는 플랫폼의 라운드 로빈을 대체로 쓰므로 지금은 안전하다.
|
||||
|
||||
그러나 두 인자 생성자가 임의의 선택기를 대체로 받고, 그 인자에는 아무 제약이 없다.
|
||||
|
||||
이 클래스의 존재 이유가 "선택기 버그가 가용성이 아니라 균형을 저하시키게 하는 것" 인데, 대체 선택기의 버그는 가용성을 저하시킨다.
|
||||
|
||||
수정은 대체 호출도 같은 검사를 지나게 하거나(그 결과가 널이거나 목록 밖이면 플랫폼 라운드 로빈으로 한 번 더 떨어진다), 두 인자 생성자를 없애 대체를 플랫폼 것으로 고정하는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 사용자 정의 선택기와 대체 선택기의 호출 지점에 걸린 방어 코드 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-advanced-resilience.md#L155 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`fallback.pick(selectable)` 은 감싸이지 않는다. 대체가 던지면 예외가 그대로 올라가고, 널이나 목록 밖 엔드포인트를 돌려주면 그대로 결정이 된다.
|
||||
|
||||
## 대체 선택기가 감싸이지 않는다
|
||||
|
||||
:::evidence key="grpc-advanced-resilience-f02" alt="분석 문서 analysis/grpc/grpc-advanced-resilience.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-advanced-resilience.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 지금은 안전하다
|
||||
|
||||
기본 생성자는 플랫폼의 라운드 로빈을 대체로 쓴다. 그러나 두 인자 생성자가 임의의 선택기를 대체로 받고, 그 인자에는 아무 제약이 없다.
|
||||
|
||||
## 클래스의 존재 이유가 뒤집힌다
|
||||
|
||||
"선택기 버그가 가용성이 아니라 균형을 저하시키게 하는 것" 인데, 대체 선택기의 버그는 가용성을 저하시킨다. 수정은 대체 호출도 같은 검사를 지나게 하거나(결과가 널이거나 목록 밖이면 플랫폼 라운드 로빈으로 한 번 더 떨어진다), 두 인자 생성자를 없애 대체를 플랫폼 것으로 고정하는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
대체 선택기가 널이나 목록 밖 엔드포인트를 돌려주는 상황을 실행으로 재현하지 않았다. 두 호출 지점의 감싸기 유무로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-advanced-streaming-f01
|
||||
title: 클래스가 비판한 무제한 증가를 형제 맵이 그대로 한다
|
||||
topic: contract-domain-and-bounds
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-advanced-streaming-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-streaming-f01
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-streaming-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-streaming-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-streaming.md#L125 이다.
|
||||
module: grpc-advanced-streaming
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 클래스가 비판한 무제한 증가를 형제 맵이 그대로 한다
|
||||
|
||||
클래스 javadoc 이 집합 방식을 거부한 이유가 무제한 증가다 — "A set grows without bound for the life of a session". 체크포인트는 그 비판을 지킨다.
|
||||
|
||||
## 문제
|
||||
|
||||
클래스 javadoc 이 집합 방식을 거부한 이유가 무제한 증가다 — "A set grows without bound for the life of a session".
|
||||
|
||||
체크포인트는 그 비판을 지킨다.
|
||||
|
||||
## 결론
|
||||
|
||||
세션당 항목 하나이고 순번만 앞으로 간다.
|
||||
|
||||
형제 맵은 지키지 않는다.
|
||||
|
||||
제거는 endSession 뿐이고, 그때 그 세션의 접두를 가진 키를 전부 지운다.
|
||||
|
||||
그러므로 결과 참조를 기록하는 세션에서는 적용된 메시지 수만큼 항목이 쌓인다.
|
||||
|
||||
상한도 만료도 없다.
|
||||
|
||||
클래스 javadoc 은 다르게 말한다.
|
||||
|
||||
작은 창이 코드에 없다.
|
||||
|
||||
체크포인트가 앞으로 가도 그 이전 결과들은 남는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 체크포인트와 형제 맵의 제거 경로 유무 대조, 클래스 javadoc 의 거부 근거 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-advanced-streaming.md#L125 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
클래스 javadoc 이 집합 방식을 거부한 이유가 무제한 증가다 — "A set grows without bound for the life of a session".
|
||||
|
||||
## javadoc 이 집합 방식을 거부한 이유
|
||||
|
||||
:::evidence key="grpc-advanced-streaming-f01" alt="분석 문서 analysis/grpc/grpc-advanced-streaming.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-advanced-streaming.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 체크포인트는 그 비판을 지키고 형제 맵은 지키지 않는다
|
||||
|
||||
체크포인트는 세션당 항목 하나이고 순번만 앞으로 간다. 형제 맵의 제거는 `endSession` 뿐이고, 그때 그 세션의 접두를 가진 키를 전부 지운다. 그러므로 결과 참조를 기록하는 세션에서는 적용된 메시지 수만큼 항목이 쌓인다 — 상한도 만료도 없다. 클래스 javadoc 은 다르게 말한다: 작은 창이 코드에 없다.
|
||||
|
||||
## 실제로 필요한 창은 좁다
|
||||
|
||||
판정이 `alreadyApplied(sequence)` 로 재생을 결정하고, 재생 응답에 쓰이는 것은 그 순번의 결과 하나다. 체크포인트보다 한참 뒤처진 순번의 결과가 필요할 상황은 재개 직후의 좁은 구간뿐이다. 수정은 창을 실제로 만드는 것이다 — 세션당 최근 N개만 유지하거나, 체크포인트가 앞으로 갈 때 그보다 오래된 항목을 지운다. 후자가 자바독의 서술과 정확히 같다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
장시간 실행으로 이 맵의 증가를 측정하지 않았다. 제거 경로가 없다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-advanced-streaming-f02
|
||||
title: 클라이언트 스트림 정책의 네 상한 중 둘은 읽는 코드가 없다
|
||||
topic: contract-domain-and-bounds
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-advanced-streaming-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-streaming-f02
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-streaming-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-streaming-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-streaming.md#L159 이다.
|
||||
module: grpc-advanced-streaming
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 클라이언트 스트림 정책의 네 상한 중 둘은 읽는 코드가 없다
|
||||
|
||||
GrpcClientStreamPolicy javadoc 이 네 상한을 모두 든다. 저장소 전체에서 접근자 호출을 세면 둘이 0 이다.
|
||||
|
||||
## 문제
|
||||
|
||||
GrpcClientStreamPolicy javadoc 이 네 상한을 모두 든다.
|
||||
|
||||
저장소 전체에서 접근자 호출을 세면 둘이 0 이다.
|
||||
|
||||
## 결론
|
||||
|
||||
Advanced 가족이 미배선이라는 사실과는 별개다 — 이 리프 안에도 그 값을 쓰는 코드가 없다.
|
||||
|
||||
수요 상한을 강제하는 GrpcDemandController 는 GrpcManualFlowControlPolicy 를 쓰고, 이 정책을 보지 않는다.
|
||||
|
||||
wholeStreamRetryAllowed() 는 항상 거짓을 돌려주는 형태이므로 그 자체가 문서화 장치다.
|
||||
|
||||
나머지 둘은 강제 지점이 필요하다.
|
||||
|
||||
수정은 상한을 강제하는 지점을 만들거나(수신 경로에 속도·미확인 수 검사), 강제되지 않는 값이 강제되는 것처럼 읽히지 않도록 자바독을 낮추는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcClientStreamPolicy 참조 10건 검색으로 네 상한의 접근자 호출 수 집계
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-advanced-streaming.md#L159 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GrpcClientStreamPolicy` javadoc 이 네 상한을 모두 든다. 저장소 전체에서 접근자 호출을 세면 둘이 0 이다.
|
||||
|
||||
## GrpcClientStreamPolicy 참조 위치
|
||||
|
||||
:::evidence key="grpc-advanced-streaming-f02" alt="코드베이스에서 GrpcClientStreamPolicy 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcClientStreamPolicy 코드베이스 검색 — 10줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## Advanced 미배선과는 별개다
|
||||
|
||||
이 리프 안에도 그 값을 쓰는 코드가 없다. 수요 상한을 강제하는 `GrpcDemandController` 는 `GrpcManualFlowControlPolicy` 를 쓰고, 이 정책을 보지 않는다.
|
||||
|
||||
## 넷 중 하나는 그 자체가 문서화 장치다
|
||||
|
||||
`wholeStreamRetryAllowed()` 는 항상 거짓을 돌려주는 형태다. 나머지 둘은 강제 지점이 필요하다. 수정은 상한을 강제하는 지점을 만들거나(수신 경로에 속도·미확인 수 검사), 강제되지 않는 값이 강제되는 것처럼 읽히지 않도록 자바독을 낮추는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제 스트림을 열어 두 상한이 무시되는 것을 관측하지 않았다. 배선 경로가 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-core-api-f05
|
||||
title: 메타데이터 예산의 두 성분 중 하나는 강제되지 않고, 나머지 하나는 바이트가 아니라 문자를 센다
|
||||
topic: contract-domain-and-bounds
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-core-api-f05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-core-api-f05
|
||||
file: ../../../final/evidence/rendered/grpc-core-api-f05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-core-api-f05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-core-api.md#L219 이다.
|
||||
module: grpc-core-api
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 메타데이터 예산의 두 성분 중 하나는 강제되지 않고, 나머지 하나는 바이트가 아니라 문자를 센다
|
||||
|
||||
GrpcMetadataBudget 은 세 성분을 갖는다 — maxTotalBytes·maxUserDefinedBytes·maxEntries. check(...) 가 보는 것은 뒤의 둘뿐이다.
|
||||
|
||||
## 문제
|
||||
|
||||
GrpcMetadataBudget 은 세 성분을 갖는다 — maxTotalBytes·maxUserDefinedBytes·maxEntries.
|
||||
|
||||
check(...) 가 보는 것은 뒤의 둘뿐이다.
|
||||
|
||||
## 결론
|
||||
|
||||
저장소 전체에서 이 접근자를 부르는 곳은 compact constructor 의 순서 가드와 테스트 단언 하나뿐이다.
|
||||
|
||||
자바독은 그 이유를 설명한다 — 하드 총계를 넘기는 것은 프레임워크가 던지는 전송 거절이고, 여기서 함께 검사하면 "고칠 수 있는 쪽" 과 "고칠 수 없는 쪽" 이 한 자리에서 발견된다는 것.
|
||||
|
||||
판단은 옳다.
|
||||
|
||||
다만 그 결과로 이 record 는 자기가 쓰지 않는 수를 성분으로 들고 있고, 이름은 그것이 강제된다고 읽힌다.
|
||||
|
||||
성분 이름은 ...Bytes 인데 세는 것은 String.length(), 즉 UTF-16 코드 단위다.
|
||||
|
||||
키는 [a-z0-9._-] 로 제한되어 ASCII 지만 값에는 문자 집합 제약이 없다.
|
||||
|
||||
다중 바이트 문자를 담은 값은 실제 프레임보다 적게 계산된다.
|
||||
|
||||
gRPC 의 ASCII 메타데이터 값은 프로토콜 상 인쇄 가능 ASCII 여야 하므로 실무에서는 대개 일치한다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcMetadataBudget 참조 26건 검색과 check 가 실제로 보는 성분 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-core-api.md#L219 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GrpcMetadataBudget` 은 세 성분을 갖는다 — `maxTotalBytes`·`maxUserDefinedBytes`·`maxEntries`. `check(...)` 가 보는 것은 뒤의 둘뿐이다. 저장소 전체에서 `maxTotalBytes` 접근자를 부르는 곳은 compact constructor 의 순서 가드와 테스트 단언 하나뿐이다.
|
||||
|
||||
## GrpcMetadataBudget 참조 위치
|
||||
|
||||
:::evidence key="grpc-core-api-f05" alt="코드베이스에서 GrpcMetadataBudget 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcMetadataBudget 코드베이스 검색 — 26줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 그 판단은 옳다
|
||||
|
||||
자바독이 이유를 설명한다 — 하드 총계를 넘기는 것은 프레임워크가 던지는 전송 거절이고, 여기서 함께 검사하면 "고칠 수 있는 쪽" 과 "고칠 수 없는 쪽" 이 한 자리에서 발견된다. 다만 그 결과로 이 record 는 자기가 쓰지 않는 수를 성분으로 들고 있고, 이름은 그것이 강제된다고 읽힌다.
|
||||
|
||||
## 이름은 Bytes 인데 세는 것은 문자다
|
||||
|
||||
`String.length()`, 즉 UTF-16 코드 단위다. 키는 `[a-z0-9._-]` 로 제한되어 ASCII 지만 값에는 문자 집합 제약이 없으므로, 다중 바이트 문자를 담은 값은 실제 프레임보다 적게 계산된다.
|
||||
|
||||
## 실무에서는 대개 일치한다
|
||||
|
||||
gRPC 의 ASCII 메타데이터 값은 프로토콜 상 인쇄 가능 ASCII 여야 한다. 다만 그 제약을 이 클래스가 검사하지 않으므로, 일치는 보장이 아니라 관행이다. 수정은 둘 다 작다 — `value.getBytes(StandardCharsets.US_ASCII).length` 로 세거나 값의 문자 집합을 `GrpcMetadataKey.Kind.ASCII` 에 맞춰 검증하고, `maxTotalBytes` 는 성분에서 빼고 javadoc 의 서술로 남긴다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
다국어 헤더 값으로 문자 수와 바이트 수의 차이를 실행으로 관측하지 않았다. 계산 대상의 코드 형태로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-discovery-f01
|
||||
title: 프로파일이 스트림 재접속 예산을 선언하는데 그것이 함의하는 DNS 갱신 주기를 정하지 않는다
|
||||
topic: contract-domain-and-bounds
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-discovery-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-discovery-f01
|
||||
file: ../../../final/evidence/rendered/grpc-discovery-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-discovery-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-discovery.md#L152 이다.
|
||||
module: grpc-discovery
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 프로파일이 스트림 재접속 예산을 선언하는데 그것이 함의하는 DNS 갱신 주기를 정하지 않는다
|
||||
|
||||
GrpcKubernetesProfile 은 세 시간 값을 다룬다. 검증기는 앞의 둘만 비교한다 — 배수 유예가 재접속 예산보다 짧으면 위반.
|
||||
|
||||
## 문제
|
||||
|
||||
GrpcKubernetesProfile 은 세 시간 값을 다룬다.
|
||||
|
||||
검증기는 앞의 둘만 비교한다 — 배수 유예가 재접속 예산보다 짧으면 위반.
|
||||
|
||||
## 결론
|
||||
|
||||
셋째는 비교 대상에 없다.
|
||||
|
||||
그래서 headlessStreaming()(재접속 예산 5초, 배수 유예 30초)에서 갱신 주기는 여전히 30초다.
|
||||
|
||||
롤아웃으로 스트림이 끊긴 클라이언트가 5초 예산 안에 재접속하려 할 때, 그 클라이언트의 DNS 캐시는 최대 30초 동안 사라진 파드 주소를 들고 있을 수 있다.
|
||||
|
||||
그 실패가 GrpcResolverProfile 자신의 javadoc 이 서술한 것이다 — "A channel that resolved once at startup keeps sending to addresses that stopped existing an hour ago; the calls fail with UNAVAILABLE and the deployment looks unhealthy long after it finished." 수정은 갱신 주기를 재접속 예산에서 파생시키거나(예: 예산 이하), 검증기에 세 값의 순서 규칙을 추가하는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcKubernetesProfile 참조 26건 검색과 검증기가 비교하는 시간 값 쌍 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-discovery.md#L152 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GrpcKubernetesProfile` 은 세 시간 값을 다룬다. 검증기는 앞의 둘만 비교한다 — 배수 유예가 재접속 예산보다 짧으면 위반.
|
||||
|
||||
## GrpcKubernetesProfile 참조 위치
|
||||
|
||||
:::evidence key="grpc-discovery-f01" alt="코드베이스에서 GrpcKubernetesProfile 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcKubernetesProfile 코드베이스 검색 — 26줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 셋째는 비교 대상에 없다
|
||||
|
||||
그래서 `headlessStreaming()`(재접속 예산 5초, 배수 유예 30초)에서 갱신 주기는 여전히 30초다.
|
||||
|
||||
## 그 조합이 만드는 상황
|
||||
|
||||
롤아웃으로 스트림이 끊긴 클라이언트가 5초 예산 안에 재접속하려 할 때, 그 클라이언트의 DNS 캐시는 최대 30초 동안 사라진 파드 주소를 들고 있을 수 있다. 그 실패가 `GrpcResolverProfile` 자신의 javadoc 이 서술한 것이다 — "A channel that resolved once at startup keeps sending to addresses that stopped existing an hour ago; the calls fail with `UNAVAILABLE` and the deployment looks unhealthy long after it finished."
|
||||
|
||||
## 수정
|
||||
|
||||
갱신 주기를 재접속 예산에서 파생시키거나(예: 예산 이하), 검증기에 세 값의 순서 규칙을 추가하는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제 DNS 리졸버로 헤드리스 레코드를 조회해 갱신 주기와 재접속 예산의 관계를 관측하지 않았다. 이 리프는 주소 수를 입력으로 받는다.
|
||||
|
||||
<!-- body:end -->
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-operation-ledger-jpa-f01
|
||||
title: 낙관적 잠금 컬럼이 없어 전이 가드가 메모리 안에만 있다
|
||||
topic: contract-domain-and-bounds
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-operation-ledger-jpa-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-operation-ledger-jpa-f01
|
||||
file: ../../../final/evidence/rendered/grpc-operation-ledger-jpa-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-operation-ledger-jpa-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-operation-ledger-jpa.md#L184 이다.
|
||||
module: grpc-operation-ledger-jpa
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 낙관적 잠금 컬럼이 없어 전이 가드가 메모리 안에만 있다
|
||||
|
||||
requireInProgress() 가 두 번째 종결 전이를 막는다. 그 가드는 한 영속성 컨텍스트 안의 인스턴스 상태에만 적용된다.
|
||||
|
||||
## 문제
|
||||
|
||||
requireInProgress() 가 두 번째 종결 전이를 막는다.
|
||||
|
||||
그 가드는 한 영속성 컨텍스트 안의 인스턴스 상태에만 적용된다.
|
||||
|
||||
## 결론
|
||||
|
||||
엔티티에 @Version 이 없으므로 두 트랜잭션이 같은 행을 읽어 각각 전이하면 나중 쓰기가 앞의 것을 덮는다.
|
||||
|
||||
DB 의 세 CHECK 제약은 행의 모양을 지키지 지 전이 순서를 지키지 않는다.
|
||||
|
||||
COMMITTED 행이 다른 결과 참조로 갱신되는 것을 막는 제약이 없다.
|
||||
|
||||
청구가 배타적이라는 설계 전제 아래서는 도달성이 낮다.
|
||||
|
||||
다만 §17.1 을 고치면 이 전제가 실제로 성립하는지가 함께 확인되어야 한다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 엔티티의 잠금 컬럼 선언 유무와 requireInProgress 가드가 적용되는 범위 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-operation-ledger-jpa.md#L184 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`requireInProgress()` 가 두 번째 종결 전이를 막는다. 그 가드는 한 영속성 컨텍스트 안의 인스턴스 상태에만 적용된다.
|
||||
|
||||
## requireInProgress() 가 적용되는 범위
|
||||
|
||||
:::evidence key="grpc-operation-ledger-jpa-f01" alt="분석 문서 analysis/grpc/grpc-operation-ledger-jpa.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-operation-ledger-jpa.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## @Version 이 없다
|
||||
|
||||
엔티티에 낙관적 잠금 컬럼이 없으므로 두 트랜잭션이 같은 행을 읽어 각각 전이하면 나중 쓰기가 앞의 것을 덮는다.
|
||||
|
||||
## DB 제약은 모양을 지키지 순서를 지키지 않는다
|
||||
|
||||
세 CHECK 제약은 행의 모양을 지킨다. `COMMITTED` 행이 다른 결과 참조로 갱신되는 것을 막는 제약이 없다.
|
||||
|
||||
## 청구가 배타적이라는 전제 아래서는 도달성이 낮다
|
||||
|
||||
다만 §17.1 을 고치면 이 전제가 실제로 성립하는지가 함께 확인되어야 한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제 데이터베이스로 청구를 두 번 돌려 재현하지 않았다. 동시 청구를 실제 커넥션 둘로도 재현하지 않았다.
|
||||
|
||||
<!-- body:end -->
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-core-api-f05
|
||||
title: WireSafeText의 규칙이 leaf 경계에서 멈춘다
|
||||
topic: contract-domain-and-bounds
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-core-api-f05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-core-api-f05
|
||||
file: ../../../final/evidence/rendered/messaging-core-api-f05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-core-api-f05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-core-api.md#L880 이다.
|
||||
module: messaging-core-api
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# WireSafeText의 규칙이 leaf 경계에서 멈춘다
|
||||
|
||||
WireSafeText의 leaf 밖 참조 0. 제어문자·인코딩 경계를 각자 검사하는 곳이 저장소에 최소 15개.
|
||||
|
||||
## 문제
|
||||
|
||||
WireSafeText의 leaf 밖 참조 0.
|
||||
|
||||
제어문자·인코딩 경계를 각자 검사하는 곳이 저장소에 최소 15개.
|
||||
|
||||
## 결론
|
||||
|
||||
javadoc이 "Each copy of this check ...
|
||||
|
||||
was one more place for the rule to drift"라고 적었고 그 통합을 leaf 안에서만 했다.
|
||||
|
||||
저장소 수준에서는 같은 drift가 그대로 남아 있다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : WireSafeText 참조 11건 검색과 같은 검사를 각자 구현한 지점 집계
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-core-api.md#L880 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`WireSafeText`의 leaf 밖 참조가 0이다. 제어문자·인코딩 경계를 각자 검사하는 곳이 저장소에 최소 15개 있다.
|
||||
|
||||
## WireSafeText 참조 위치
|
||||
|
||||
:::evidence key="messaging-core-api-f05" alt="코드베이스에서 WireSafeText 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WireSafeText 코드베이스 검색 — 11줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## javadoc 이 통합을 이유로 들었다
|
||||
|
||||
"Each copy of this check ... was one more place for the rule to drift" 라고 적었고, 그 통합을 leaf 안에서만 했다. 저장소 수준에서는 같은 drift가 그대로 남아 있다.
|
||||
|
||||
## 후보
|
||||
|
||||
규칙을 공유 위치(`shared-contract`)로 올리거나, leaf 경계를 이유로 중복을 명시적으로 수용한다고 적는다. 저장소 전역 판단이므로 cross-scope 소유이고 여기서는 관측만 기록한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
각 지점의 검사가 실제로 이 규칙과 어긋나는 입력을 통과시키는지 확인하지 않았다. 참조 경계와 구현 지점 수로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-observability-f07
|
||||
title: extract가 손상된 추적 헤더에 분류되지 않은 예외를 던진다
|
||||
topic: contract-domain-and-bounds
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-observability-f07
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-observability-f07
|
||||
file: ../../../final/evidence/rendered/messaging-observability-f07.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-observability-f07.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-observability.md#L726 이다.
|
||||
module: messaging-observability
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# extract가 손상된 추적 헤더에 분류되지 않은 예외를 던진다
|
||||
|
||||
MessagingTracer.extract가 new TraceContext(...)를 부르고, 그 생성자는 W3C 문법·바이트 상한·all-zero를 검사해 IllegalArgumentException을 던진다. extract는 잡지 않는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **타입이 문서화한 불변식은 타입이 강제한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
MessagingTracer.extract가 new TraceContext(...)를 부르고, 그 생성자는 W3C 문법·바이트 상한·all-zero를 검사해 IllegalArgumentException을 던진다.
|
||||
|
||||
extract는 잡지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
다른 시스템이 보낸 메시지의 헤더는 신뢰할 수 없는 입력이다.
|
||||
|
||||
손상된 traceparent 하나가 MessagingException이 아닌 예외로 소비 경로를 끊는다 — 추적이 없어야 할 자리에서 메시지 처리가 실패한다.
|
||||
|
||||
messaging-cloudevents의 id 파싱과 같은 형태다(그쪽 §17).
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : MessagingTracer 참조 4건 검색과 생성자가 던지는 조건 및 extract 의 catch 유무 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-observability.md#L726 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`MessagingTracer.extract`가 `new TraceContext(...)`를 부르고, 그 생성자는 W3C 문법·바이트 상한·all-zero를 검사해 `IllegalArgumentException`을 던진다. `extract`는 잡지 않는다.
|
||||
|
||||
## MessagingTracer 참조 위치
|
||||
|
||||
:::evidence key="messaging-observability-f07" alt="코드베이스에서 MessagingTracer 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingTracer 코드베이스 검색 — 4줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 신뢰할 수 없는 입력이다
|
||||
|
||||
다른 시스템이 보낸 메시지의 헤더다. 손상된 `traceparent` 하나가 `MessagingException`이 아닌 예외로 소비 경로를 끊는다 — 추적이 없어야 할 자리에서 메시지 처리가 실패한다. `messaging-cloudevents`의 id 파싱과 같은 형태다(그쪽 §17).
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
손상된 traceparent 가 실제 배포에서 얼마나 자주 오는지 확인하지 않았다. 생성자의 검사와 extract 의 예외 처리 부재로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-reliability-api-f05
|
||||
title: inbox 보존 규칙이 문서로만 있다
|
||||
topic: contract-domain-and-bounds
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-reliability-api-f05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-reliability-api-f05
|
||||
file: ../../../final/evidence/rendered/messaging-reliability-api-f05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-reliability-api-f05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-reliability-api.md#L725 이다.
|
||||
module: messaging-reliability-api
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# inbox 보존 규칙이 문서로만 있다
|
||||
|
||||
InboxRepository.purgeProcessedBefore javadoc이 "Retention must outlive the broker's maximum redelivery window, otherwise a late redelivery arrives after its inbox row was pruned and is processed a second time"라고 한다. 그 비교를 하는 코드가 이 leaf에도 messaging-policy의 프로파일 검증기에도 없다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **record의 `equals`를 좁히면 이유를 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
InboxRepository.purgeProcessedBefore javadoc이 "Retention must outlive the broker's maximum redelivery window, otherwise a late redelivery arrives after its inbox row was pruned and is processed a second time"라고 한다.
|
||||
|
||||
그 비교를 하는 코드가 이 leaf에도 messaging-policy의 프로파일 검증기에도 없다.
|
||||
|
||||
## 결론
|
||||
|
||||
위반의 결과가 부작용의 이중 실행이다 — Inbox가 존재하는 이유 그 자체가 무효화된다.
|
||||
|
||||
그리고 위반이 조용하다: 짧은 보존은 정상 동작처럼 보이고 늦은 재전달이 올 때만 드러난다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : InboxRepository 참조 20건 검색과 보존·재전달 창을 비교하는 코드의 존재 여부 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-reliability-api.md#L725 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`InboxRepository.purgeProcessedBefore` javadoc이 "Retention must outlive the broker's maximum redelivery window, otherwise a late redelivery arrives after its inbox row was pruned and is processed a second time"라고 한다. 그 비교를 하는 코드가 이 leaf에도 `messaging-policy`의 프로파일 검증기에도 없다.
|
||||
|
||||
## InboxRepository 참조 위치
|
||||
|
||||
:::evidence key="messaging-reliability-api-f05" alt="코드베이스에서 InboxRepository 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxRepository 코드베이스 검색 — 20줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 위반의 결과가 부작용의 이중 실행이다
|
||||
|
||||
Inbox가 존재하는 이유 그 자체가 무효화된다. 그리고 위반이 조용하다 — 짧은 보존은 정상 동작처럼 보이고 늦은 재전달이 올 때만 드러난다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
inbox 보존 기간이 실제 배포에서 브로커 재전달 창보다 긴지 확인할 수 없었다. 비교하는 코드가 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-reliability-api-f08
|
||||
title: 포트가 bounded/unbounded purge 두 오버로드를 나란히 노출하고, 호출자가 무제한 쪽을 고른다
|
||||
topic: contract-domain-and-bounds
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-reliability-api-f08
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-reliability-api-f08
|
||||
file: ../../../final/evidence/rendered/messaging-reliability-api-f08.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-reliability-api-f08.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-reliability-api.md#L752 이다.
|
||||
module: messaging-reliability-api
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 포트가 bounded/unbounded purge 두 오버로드를 나란히 노출하고, 호출자가 무제한 쪽을 고른다
|
||||
|
||||
InboxRepository와 OutboxRepository가 각각 purge*Before(Instant)와 purge*Before(Instant, int)를 선언한다. 후자에 호출 지점이 0이고 두 cleanup job이 전자를 부른다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **record의 `equals`를 좁히면 이유를 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
InboxRepository와 OutboxRepository가 각각 purge*Before(Instant)와 purge*Before(Instant, int)를 선언한다.
|
||||
|
||||
후자에 호출 지점이 0이고 두 cleanup job이 전자를 부른다.
|
||||
|
||||
## 결론
|
||||
|
||||
§12.1(a)의 두 세대 전이와 같은 형태다 — 한 인터페이스가 안전한 형태와 그렇지 않은 형태를 나란히 두고, @Deprecated도 이름 차이도 없으며, 호출자가 짧은 쪽을 골랐다.
|
||||
|
||||
두 경우 모두 포트의 형태가 오용을 가능하게 했다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : InboxRepository 참조 20건 검색과 두 오버로드의 호출 지점 집계
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-reliability-api.md#L752 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`InboxRepository`와 `OutboxRepository`가 각각 `purge*Before(Instant)`와 `purge*Before(Instant, int)`를 선언한다. 후자에 호출 지점이 0이고 두 cleanup job이 전자를 부른다.
|
||||
|
||||
## InboxRepository 참조 위치
|
||||
|
||||
:::evidence key="messaging-reliability-api-f08" alt="코드베이스에서 InboxRepository 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxRepository 코드베이스 검색 — 20줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## §12.1(a)의 두 세대 전이와 같은 형태다
|
||||
|
||||
한 인터페이스가 안전한 형태와 그렇지 않은 형태를 나란히 두고, `@Deprecated`도 이름 차이도 없으며, 호출자가 짧은 쪽을 골랐다. 두 경우 모두 포트의 형태가 오용을 가능하게 했다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
무제한 오버로드가 실제 백로그에서 무엇을 잠그는지 측정하지 않았다. 판정은 구현 리프의 SSOT 가 소유하고 여기서는 포트 형태의 기여만 남겼다.
|
||||
|
||||
<!-- body:end -->
|
||||
Reference in New Issue
Block a user