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
+78
@@ -0,0 +1,78 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-advanced-bootstrap-f05
|
||||
title: 예외가 들고 있는 능력이 transient 라 역직렬화 뒤 사라진다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-advanced-bootstrap-f05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-bootstrap-f05
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-bootstrap-f05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-bootstrap-f05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-bootstrap.md#L312 이다.
|
||||
module: grpc-advanced-bootstrap
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 예외가 들고 있는 능력이 transient 라 역직렬화 뒤 사라진다
|
||||
|
||||
transient 는 보통 직렬화 가능하지 않은 필드를 담은 Serializable 클래스에 대한 정적 분석 경고를 끄려고 붙인다. 그런데 열거형은 언제나 직렬화 가능하다 — 여기서 transient 가 막을 문제가 애초에 없다.
|
||||
|
||||
## 문제
|
||||
|
||||
transient 는 보통 직렬화 가능하지 않은 필드를 담은 Serializable 클래스에 대한 정적 분석 경고를 끄려고 붙인다.
|
||||
|
||||
그런데 열거형은 언제나 직렬화 가능하다 — 여기서 transient 가 막을 문제가 애초에 없다.
|
||||
|
||||
## 결론
|
||||
|
||||
대가는 있다.
|
||||
|
||||
예외가 직렬화를 거쳐 오면 capability() 가 null 이다.
|
||||
|
||||
메시지 문자열은 살아남으므로 사람이 읽는 데는 지장이 없고, 그래서 눈에 띄지 않는다.
|
||||
|
||||
이 예외를 던지는 require 자체가 리프 밖에서 불리지 않으므로(§12.1) 오늘 도달하지 않는다.
|
||||
|
||||
transient 를 지우는 것이 수정 전부다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : Serializable 참조 8건 검색과 예외 필드의 transient 선언 및 열거형 직렬화 성질 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-advanced-bootstrap.md#L312 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`transient` 는 보통 직렬화 가능하지 않은 필드를 담은 `Serializable` 클래스에 대한 정적 분석 경고를 끄려고 붙인다. 그런데 열거형은 언제나 직렬화 가능하다 — 여기서 `transient` 가 막을 문제가 애초에 없다.
|
||||
|
||||
## Serializable 참조 위치
|
||||
|
||||
:::evidence key="grpc-advanced-bootstrap-f05" alt="코드베이스에서 Serializable 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="Serializable 코드베이스 검색 — 8줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 대가는 있다
|
||||
|
||||
예외가 직렬화를 거쳐 오면 `capability()` 가 `null` 이다. 메시지 문자열은 살아남으므로 사람이 읽는 데는 지장이 없고, 그래서 눈에 띄지 않는다. 이 예외를 던지는 `require` 자체가 리프 밖에서 불리지 않으므로(§12.1) 오늘 도달하지 않는다.
|
||||
|
||||
## 수정
|
||||
|
||||
`transient` 를 지우는 것이 전부다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제로 직렬화·역직렬화해 능력이 사라지는 것을 관측하지 않았다. 열거형이 언제나 직렬화 가능하다는 것과 transient 선언의 대조로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-advanced-edition-f01
|
||||
title: 비교 픽스처에 비교 대상이 없다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-advanced-edition-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-edition-f01
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-edition-f01.svg
|
||||
- key: grpc-advanced-edition-f01-diagram
|
||||
file: ../../../final/assets/diagrams/grpc-advanced-edition-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-edition-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-edition.md#L180 이다.
|
||||
module: grpc-advanced-edition
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 비교 픽스처에 비교 대상이 없다
|
||||
|
||||
compatibility.proto 의 주석이 존재 이유를 적는다. 그 쌍둥이가 저장소에 없다.
|
||||
|
||||
## 문제
|
||||
|
||||
compatibility.proto 의 주석이 존재 이유를 적는다.
|
||||
|
||||
그 쌍둥이가 저장소에 없다.
|
||||
|
||||
## 결론
|
||||
|
||||
한 곳뿐이다.
|
||||
|
||||
같은 필드와 번호를 proto3 로 선언한 파일이 없으므로 비교가 성립하지 않는다.
|
||||
|
||||
그리고 두 번째 전제도 없다.
|
||||
|
||||
이 저장소에는 protobuf 플러그인이 어디에도 없다 — grpc-proto-contract 와 adapter-inbound-grpc 의 build.gradle 이 그 사실을 주석으로 명시한다.
|
||||
|
||||
그러므로 편집 파일도 proto3 파일도 컴파일되지 않고, 유선 바이트와 JSON 을 비교할 산출물 자체가 만들어지지 않는다.
|
||||
|
||||
결과적으로 GrpcEditionCompatibilityReport 는 사람이 손으로 채우는 기록이 된다.
|
||||
|
||||
승격 게이트가 그것을 읽어 판정하므로, 게이트의 입력이 측정이 아니라 선언이다.
|
||||
|
||||
Advanced 가족이라 오늘의 배포에는 영향이 없다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcEditionCompatibilityReport 참조 7건 검색과 픽스처 디렉터리의 파일 목록 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-advanced-edition.md#L180 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`compatibility.proto` 의 주석이 존재 이유를 적는다. 그 쌍둥이가 저장소에 없다.
|
||||
|
||||
## 비교에 필요한 것
|
||||
|
||||
:::evidence key="grpc-advanced-edition-f01-diagram" alt="edition 픽스처만 저장소 안에 놓이고 proto3 짝 픽스처와 컴파일 산출물이 바깥에 빗금으로 놓인다" caption="비교에 필요한 것" zoom="false"
|
||||
:::
|
||||
|
||||
같은 필드와 번호를 proto3 로 선언한 파일이 없으므로 비교가 성립하지 않는다.
|
||||
|
||||
## GrpcEditionCompatibilityReport 참조 위치
|
||||
|
||||
:::evidence key="grpc-advanced-edition-f01" alt="코드베이스에서 GrpcEditionCompatibilityReport 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcEditionCompatibilityReport 코드베이스 검색 — 7줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 두 번째 전제도 없다
|
||||
|
||||
이 저장소에는 protobuf 플러그인이 어디에도 없다 — `grpc-proto-contract` 와 `adapter-inbound-grpc` 의 build.gradle 이 그 사실을 주석으로 명시한다. 그러므로 편집 파일도 proto3 파일도 컴파일되지 않고, 유선 바이트와 JSON 을 비교할 산출물 자체가 만들어지지 않는다.
|
||||
|
||||
## 게이트의 입력이 측정이 아니라 선언이다
|
||||
|
||||
`GrpcEditionCompatibilityReport` 는 사람이 손으로 채우는 기록이 되고, 승격 게이트가 그것을 읽어 판정한다. Advanced 가족이라 오늘의 배포에는 영향이 없다.
|
||||
|
||||
## 기록하는 이유
|
||||
|
||||
이 리프의 목적이 "공개 서비스가 옮겨 가기 전에 그 실패를 찾는 것" 이고, 그 실패를 찾을 장치가 픽스처 하나만 있고 짝이 없다. `compatibility_proto3.proto` 를 같은 디렉터리에 두어 필드·번호·JSON 이름을 맞추고, 두 파일을 컴파일해 산출물을 비교하는 레인을 만든다. 그 레인이 생기기 전까지는 이 보고서가 측정이 아니라 선언이라는 것을 자바독에 적는 편이 낫다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
protoc 을 돌려 이 편집 파일이 실제로 컴파일되는지 확인하지 않았다. 저장소에 protobuf 플러그인이 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-codegen-f03
|
||||
title: 픽스처의 메서드 경로가 서비스 × 메서드 교차곱이다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-codegen-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-codegen-f03
|
||||
file: ../../../final/evidence/rendered/grpc-codegen-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-codegen-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-codegen.md#L239 이다.
|
||||
module: grpc-codegen
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 픽스처의 메서드 경로가 서비스 × 메서드 교차곱이다
|
||||
|
||||
stub.<name>( 호출 하나가 그 파일이 import 한 모든 서비스에 대해 메서드 경로를 만든다. javadoc 의 규칙 서술은 단수형이다 — "a method is a stub.<name>( call, mapped to <service>/<UpperCamelName>".
|
||||
|
||||
## 문제
|
||||
|
||||
stub.<name>( 호출 하나가 그 파일이 import 한 모든 서비스에 대해 메서드 경로를 만든다.
|
||||
|
||||
javadoc 의 규칙 서술은 단수형이다 — "a method is a stub.<name>( call, mapped to <service>/<UpperCamelName>".
|
||||
|
||||
## 결론
|
||||
|
||||
서비스가 둘 이상일 때 어느 서비스인지는 소스 텍스트만으로 알 수 없고, 코드는 전부에 붙이는 쪽을 골랐다.
|
||||
|
||||
결과는 존재하지 않는 메서드 경로를 요구하는 픽스처다.
|
||||
|
||||
서비스 둘과 메서드 셋이면 요구 경로가 여섯 개가 되고, 그중 셋은 어떤 후보 스키마에도 없으므로 breaksAgainst 가 항상 METHOD_PATH 파괴를 보고한다.
|
||||
|
||||
그러면 GrpcSchemaArtifactPublisher.evaluate 가 모든 발행을 거부한다.
|
||||
|
||||
커밋된 픽스처는 서비스가 하나(DocumentServiceGrpc)라 지금은 정확하다.
|
||||
|
||||
두 번째 소비자 픽스처를 추가하는 순간 성립한다.
|
||||
|
||||
수정은 호출자 변수의 선언 타입을 함께 읽어 메서드를 서비스에 귀속시키거나, 서비스가 둘 이상인 픽스처를 거부하는 것이다.
|
||||
|
||||
후자는 지금 형태의 근사를 명시적으로 만든다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcSchemaArtifactPublisher 참조 19건 검색과 메서드 경로 생성 규칙 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-codegen.md#L239 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`stub.<name>(` 호출 하나가 그 파일이 import 한 **모든** 서비스에 대해 메서드 경로를 만든다. javadoc 의 규칙 서술은 단수형이다 — "a method is a `stub.<name>(` call, mapped to `<service>/<UpperCamelName>`".
|
||||
|
||||
## GrpcSchemaArtifactPublisher 참조 위치
|
||||
|
||||
:::evidence key="grpc-codegen-f03" alt="코드베이스에서 GrpcSchemaArtifactPublisher 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcSchemaArtifactPublisher 코드베이스 검색 — 19줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 소스 텍스트만으로는 알 수 없어 전부에 붙였다
|
||||
|
||||
서비스가 둘 이상일 때 어느 서비스인지는 소스 텍스트만으로 알 수 없고, 코드는 전부에 붙이는 쪽을 골랐다. 결과는 존재하지 않는 메서드 경로를 요구하는 픽스처다 — 서비스 둘과 메서드 셋이면 요구 경로가 여섯 개가 되고, 그중 셋은 어떤 후보 스키마에도 없으므로 `breaksAgainst` 가 항상 `METHOD_PATH` 파괴를 보고한다. 그러면 `GrpcSchemaArtifactPublisher.evaluate` 가 모든 발행을 거부한다.
|
||||
|
||||
## 지금 픽스처는 서비스가 하나라 정확하다
|
||||
|
||||
`DocumentServiceGrpc` 하나다. 두 번째 소비자 픽스처를 추가하는 순간 성립한다. 수정은 호출자 변수의 선언 타입을 함께 읽어 메서드를 서비스에 귀속시키거나, 서비스가 둘 이상인 픽스처를 거부하는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
서비스가 둘 이상인 픽스처를 만들어 교차곱을 재현하지 않았다. 유도 코드로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-core-api-f06
|
||||
title: 직렬화 가능하다고 선언한 예외가 자기 내용을 직렬화하지 않는다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-core-api-f06
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-core-api-f06
|
||||
file: ../../../final/evidence/rendered/grpc-core-api-f06.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-core-api-f06.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-core-api.md#L241 이다.
|
||||
module: grpc-core-api
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 직렬화 가능하다고 선언한 예외가 자기 내용을 직렬화하지 않는다
|
||||
|
||||
serialVersionUID 는 이 타입이 직렬화된다는 선언이고, transient 는 유일한 필드가 그 직렬화에서 빠진다는 선언이다. 둘이 함께 있으면 역직렬화된 예외는 context == null 이고, 공개 메서드 둘 중 하나(requiresReconciliation())가 NPE 를 던진다.
|
||||
|
||||
## 문제
|
||||
|
||||
serialVersionUID 는 이 타입이 직렬화된다는 선언이고, transient 는 유일한 필드가 그 직렬화에서 빠진다는 선언이다.
|
||||
|
||||
둘이 함께 있으면 역직렬화된 예외는 context == null 이고, 공개 메서드 둘 중 하나(requiresReconciliation())가 NPE 를 던진다.
|
||||
|
||||
## 결론
|
||||
|
||||
transient 자체는 강제된 선택이다 — GrpcFailureContext 가 Serializable 을 구현하지 않으므로 필드를 남기면 예외가 직렬화되지 않는다.
|
||||
|
||||
기록하는 이유는 이 리프의 서술 규율과 대비되기 때문이다.
|
||||
|
||||
다른 자리에서는 부재마다 이유가 붙어 있다("There is no factory that takes raw metadata, and that absence is the design").
|
||||
|
||||
여기에는 transient 의 이유도, 역직렬화 뒤의 계약도 적혀 있지 않다.
|
||||
|
||||
도달성은 낮다.
|
||||
|
||||
gRPC 예외가 자바 직렬화를 지나는 경로는 이 저장소에 없다.
|
||||
|
||||
수정은 셋 중 하나다 — GrpcFailureContext 와 그 구성 요소를 Serializable 로 만들거나, serialVersionUID 를 지워 직렬화를 지원하지 않음을 명시하거나, context() 와 requiresReconciliation() 이 null 문맥을 다루도록 하고 그 이유를 적는 것.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcFailureContext 참조 34건 검색과 예외 필드의 transient·serialVersionUID 선언 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-core-api.md#L241 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`serialVersionUID` 는 이 타입이 직렬화된다는 선언이고, `transient` 는 유일한 필드가 그 직렬화에서 빠진다는 선언이다. 둘이 함께 있으면 역직렬화된 예외는 `context == null` 이고, 공개 메서드 둘 중 하나(`requiresReconciliation()`)가 NPE 를 던진다.
|
||||
|
||||
## GrpcFailureContext 참조 위치
|
||||
|
||||
:::evidence key="grpc-core-api-f06" alt="코드베이스에서 GrpcFailureContext 를 검색한 출력 34줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcFailureContext 코드베이스 검색 — 34줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## transient 자체는 강제된 선택이다
|
||||
|
||||
`GrpcFailureContext` 가 `Serializable` 을 구현하지 않으므로 필드를 남기면 예외가 직렬화되지 않는다.
|
||||
|
||||
## 이 리프의 서술 규율과 대비된다
|
||||
|
||||
다른 자리에서는 부재마다 이유가 붙어 있다("There is no factory that takes raw metadata, and that absence is the design"). 여기에는 `transient` 의 이유도, 역직렬화 뒤의 계약도 적혀 있지 않다.
|
||||
|
||||
## 도달성은 낮다
|
||||
|
||||
gRPC 예외가 자바 직렬화를 지나는 경로는 이 저장소에 없다. 수정은 셋 중 하나다 — `GrpcFailureContext` 와 그 구성 요소를 `Serializable` 로 만들거나, `serialVersionUID` 를 지워 직렬화를 지원하지 않음을 명시하거나, 두 메서드가 null 문맥을 다루도록 하고 그 이유를 적는 것.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제로 직렬화·역직렬화해 NPE 를 재현하지 않았다. 두 선언이 함께 있다는 것과 공개 메서드의 필드 접근으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-policy-f03
|
||||
title: 직렬 스트림 기록기의 가장 오래된 것 버리기가 잘못된 메시지의 바이트를 뺀다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-policy-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-policy-f03
|
||||
file: ../../../final/evidence/rendered/grpc-policy-f03.svg
|
||||
- key: grpc-policy-f03-diagram
|
||||
file: ../../../final/assets/diagrams/grpc-policy-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-policy-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-policy.md#L249 이다.
|
||||
module: grpc-policy
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 직렬 스트림 기록기의 가장 오래된 것 버리기가 잘못된 메시지의 바이트를 뺀다
|
||||
|
||||
버려지는 것은 꺼낸 봉투인데 빼는 값은 새 메시지의 크기다. 봉투는 크기를 성분으로 담지 않으므로 이 지점에서 버려지는 크기를 알 방법이 없다.
|
||||
|
||||
## 문제
|
||||
|
||||
버려지는 것은 꺼낸 봉투인데 빼는 값은 새 메시지의 크기다.
|
||||
|
||||
봉투는 크기를 성분으로 담지 않으므로 이 지점에서 버려지는 크기를 알 방법이 없다.
|
||||
|
||||
## 결론
|
||||
|
||||
계산을 따라가면 이렇다.
|
||||
|
||||
한 번의 DROP_OLDEST 마다 queuedBytes 는 nextBytes 만큼 빠졌다가 enqueue 에서 같은 값만큼 다시 더해진다 — 순변화 0.
|
||||
|
||||
그런데 큐의 실제 내용은 nextBytes - droppedBytes 만큼 바뀐다.
|
||||
|
||||
그 차이가 매 낙차마다 쌓인다.
|
||||
|
||||
방향은 둘 다 틀렸다.
|
||||
|
||||
들어오는 메시지가 버려지는 것보다 크면 추적값이 실제보다 낮아져 바이트 경계가 늦게 발화한다(메모리).
|
||||
|
||||
반대면 실제보다 높아져 경계가 이르게 발화한다(불필요한 종료·낙차).
|
||||
|
||||
누적 바이트는 흐름 제어 정책의 판정 입력이고, 바이트 경계의 존재 이유가 javadoc 에 있다 — 개수 경계만 있으면 메모리 한도를 가장 큰 메시지가 정한다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 큐에서 빼는 대상과 계수기에서 차감하는 값의 대조, 봉투 성분 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-policy.md#L249 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
버려지는 것은 꺼낸 봉투인데 빼는 값은 새 메시지의 크기다. 봉투는 크기를 성분으로 담지 않으므로 이 지점에서 버려지는 크기를 알 방법이 없다.
|
||||
|
||||
## 두 자리가 가리키는 대상
|
||||
|
||||
:::evidence key="grpc-policy-f03-diagram" alt="큐에서 빠지는 것 쪽에 가장 오래된 봉투와 그 봉투의 바이트가 놓이고 차감되는 값 쪽에 새 메시지와 그 바이트가 놓인다" caption="두 자리가 가리키는 대상" zoom="false"
|
||||
:::
|
||||
|
||||
## 계산을 따라가면
|
||||
|
||||
한 번의 DROP_OLDEST 마다 `queuedBytes` 는 `nextBytes` 만큼 빠졌다가 `enqueue` 에서 같은 값만큼 다시 더해진다 — **순변화 0**. 그런데 큐의 실제 내용은 `nextBytes - droppedBytes` 만큼 바뀐다. 그 차이가 매 낙차마다 쌓인다.
|
||||
|
||||
## 빼는 값과 버리는 대상
|
||||
|
||||
:::evidence key="grpc-policy-f03" alt="분석 문서 analysis/grpc/grpc-policy.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-policy.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 방향은 둘 다 틀렸다
|
||||
|
||||
들어오는 메시지가 버려지는 것보다 크면 추적값이 실제보다 **낮아져** 바이트 경계가 늦게 발화한다(메모리). 반대면 실제보다 **높아져** 경계가 이르게 발화한다(불필요한 종료·낙차). 누적 바이트는 흐름 제어 정책의 판정 입력이고, 바이트 경계의 존재 이유가 javadoc 에 있다 — 개수 경계만 있으면 메모리 한도를 가장 큰 메시지가 정한다.
|
||||
|
||||
## flush 가 오차를 끊는다
|
||||
|
||||
`flush()` 가 큐를 비우면서 `queuedBytes = 0L` 로 되돌리므로 오차가 flush 를 건너 누적되지는 않는다. 그래서 이것은 영구 드리프트가 아니라 한 flush 주기 안의 폭주 구간에서 바이트 경계를 잘못 판정하는 결함이다. 낙차가 일어나는 상황이 곧 소비자가 못 따라가는 상황이고, 그때 flush 간격이 가장 길어진다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제 스트림으로 버리기를 유발해 바이트 오차를 관측하지 않았다. 봉투가 크기를 성분으로 담지 않는다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-proto-contract-f01
|
||||
title: reserved 2 to 5; 범위가 개별 숫자로만 수집되어 RESERVED_HISTORY 오탐이 된다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-proto-contract-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-proto-contract-f01
|
||||
file: ../../../final/evidence/rendered/grpc-proto-contract-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-proto-contract-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-proto-contract.md#L175 이다.
|
||||
module: grpc-proto-contract
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# reserved 2 to 5; 범위가 개별 숫자로만 수집되어 RESERVED_HISTORY 오탐이 된다
|
||||
|
||||
reserved 2 to 5; 는 그룹이 "2 to 5" 이고 수집되는 것은 {2, 5} 다. 3·4 는 들어가지 않는다.
|
||||
|
||||
## 문제
|
||||
|
||||
reserved 2 to 5; 는 그룹이 "2 to 5" 이고 수집되는 것은 {2, 5} 다.
|
||||
|
||||
3·4 는 들어가지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
reserved 9 to max; 는 {9} 만 남는다.
|
||||
|
||||
그러면 삭제 이력이 3 을 담고 스키마가 reserved 2 to 5; 로 정확히 예약했는데도 RESERVED_HISTORY 위반이 보고된다.
|
||||
|
||||
범위 예약은 표준 문법이고 여러 필드를 한 번에 지울 때 쓰는 형태이므로 도달 가능하다.
|
||||
|
||||
수정은 to 를 인식해 범위를 펼치는 것이다.
|
||||
|
||||
max 는 상한 상수로 다루거나 그 메시지에 대해 검사를 통과시킨다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : reserved 범위 표기의 정규식 그룹과 수집 코드가 남기는 값의 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-proto-contract.md#L175 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`reserved 2 to 5;` 는 그룹이 `"2 to 5"` 이고 수집되는 것은 `{2, 5}` 다. `3`·`4` 는 들어가지 않는다. `reserved 9 to max;` 는 `{9}` 만 남는다.
|
||||
|
||||
## 범위 표기가 수집되는 방식
|
||||
|
||||
:::evidence key="grpc-proto-contract-f01" alt="분석 문서 analysis/grpc/grpc-proto-contract.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-proto-contract.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 정확히 예약한 스키마가 위반으로 보고된다
|
||||
|
||||
삭제 이력이 `3` 을 담고 스키마가 `reserved 2 to 5;` 로 예약했는데도 `RESERVED_HISTORY` 위반이 나온다. 범위 예약은 표준 문법이고 여러 필드를 한 번에 지울 때 쓰는 형태이므로 도달 가능하다.
|
||||
|
||||
## 수정
|
||||
|
||||
`to` 를 인식해 범위를 펼치는 것이다. `max` 는 상한 상수로 다루거나 그 메시지에 대해 검사를 통과시킨다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
범위 문법의 오탐을 실행으로 재현하지 않았다. 정규식과 수집 코드로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-proto-contract-f03
|
||||
title: 커밋 스키마 게이트가 파일 목록을 하드코딩한다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-proto-contract-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-proto-contract-f03
|
||||
file: ../../../final/evidence/rendered/grpc-proto-contract-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-proto-contract-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-proto-contract.md#L203 이다.
|
||||
module: grpc-proto-contract
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 커밋 스키마 게이트가 파일 목록을 하드코딩한다
|
||||
|
||||
리소스 디렉터리를 훑지 않는다. 이 리프에 세 번째 .proto 를 추가하면 이 테스트를 함께 고치기 전까지 판정되지 않고, 빌드는 초록으로 남는다.
|
||||
|
||||
## 문제
|
||||
|
||||
리소스 디렉터리를 훑지 않는다.
|
||||
|
||||
이 리프에 세 번째 .proto 를 추가하면 이 테스트를 함께 고치기 전까지 판정되지 않고, 빌드는 초록으로 남는다.
|
||||
|
||||
## 결론
|
||||
|
||||
같은 저장소가 다른 곳에서 이 형태를 이미 경계했다 — 빠뜨림이 통과가 되는 게이트다.
|
||||
|
||||
수정은 proto/** 아래 .proto 를 전부 열거해 돌리는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 게이트가 판정하는 파일 목록과 리소스 디렉터리의 .proto 목록 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-proto-contract.md#L203 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
게이트가 파일 목록을 하드코딩한다.
|
||||
|
||||
```java
|
||||
List<String> files = List.of(
|
||||
"proto/hyeonworks/grpc/common/v1/error.proto",
|
||||
"proto/hyeonworks/grpc/common/v1/stream.proto");
|
||||
```
|
||||
|
||||
## 게이트가 하드코딩한 목록
|
||||
|
||||
:::evidence key="grpc-proto-contract-f03" alt="분석 문서 analysis/grpc/grpc-proto-contract.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-proto-contract.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 세 번째 파일은 판정되지 않는다
|
||||
|
||||
리소스 디렉터리를 훑지 않으므로, 이 리프에 세 번째 `.proto` 를 추가하면 이 테스트를 함께 고치기 전까지 판정되지 않고 빌드는 초록으로 남는다.
|
||||
|
||||
## 같은 저장소가 이미 경계한 형태다
|
||||
|
||||
빠뜨림이 통과가 되는 게이트다. 수정은 `proto/**` 아래 `.proto` 를 전부 열거해 돌리는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
세 번째 .proto 를 추가해 게이트가 침묵하는 것을 재현하지 않았다. 목록이 하드코딩이고 디렉터리를 훑지 않는다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-proto-contract-f04
|
||||
title: 열거형 안의 reserved 는 수집되지 않는다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-proto-contract-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-proto-contract-f04
|
||||
file: ../../../final/evidence/rendered/grpc-proto-contract-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-proto-contract-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-proto-contract.md#L265 이다.
|
||||
module: grpc-proto-contract
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 열거형 안의 reserved 는 수집되지 않는다
|
||||
|
||||
scan 은 스코프 종류로 갈라진다. reserved 수집은 scanMessageMember 안에만 있다.
|
||||
|
||||
## 문제
|
||||
|
||||
scan 은 스코프 종류로 갈라진다.
|
||||
|
||||
reserved 수집은 scanMessageMember 안에만 있다.
|
||||
|
||||
## 결론
|
||||
|
||||
proto3 는 열거형에도 reserved 2, 15; 와 reserved "OLD_VALUE"; 를 허용하고, 열거형 값을 지울 때 번호를 예약하는 것은 필드와 같은 이유로 필요하다 — 예약하지 않고 재사용하면 옛 클라이언트가 보낸 정수가 다른 뜻으로 해석된다.
|
||||
|
||||
지금 SchemaHistory 에 열거형 이름으로 삭제 이력을 넣으면, 스키마가 정확히 예약했더라도 scan.reservedNumbers 에 그 이름이 없으므로 RESERVED_HISTORY 오탐이 난다.
|
||||
|
||||
§17.1 의 범위 문법 문제와 같은 방향(fail-closed)이고 같은 자리에서 고칠 수 있다.
|
||||
|
||||
reserved 수집을 스코프 종류와 무관하게 먼저 시도한 뒤 나머지 판정을 갈래로 보낸다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : SchemaHistory 참조 10건 검색과 scan 의 스코프 분기별 reserved 수집 위치 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-proto-contract.md#L265 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`scan` 은 스코프 종류로 갈라진다. `reserved` 수집은 `scanMessageMember` 안에만 있다.
|
||||
|
||||
## SchemaHistory 참조 위치
|
||||
|
||||
:::evidence key="grpc-proto-contract-f04" alt="코드베이스에서 SchemaHistory 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SchemaHistory 코드베이스 검색 — 10줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## proto3 는 열거형에도 reserved 를 허용한다
|
||||
|
||||
`reserved 2, 15;` 와 `reserved "OLD_VALUE";` 다. 열거형 값을 지울 때 번호를 예약하는 것은 필드와 같은 이유로 필요하다 — 예약하지 않고 재사용하면 옛 클라이언트가 보낸 정수가 다른 뜻으로 해석된다.
|
||||
|
||||
## 그래서 오탐이 난다
|
||||
|
||||
`SchemaHistory` 에 열거형 이름으로 삭제 이력을 넣으면, 스키마가 정확히 예약했더라도 `scan.reservedNumbers` 에 그 이름이 없으므로 `RESERVED_HISTORY` 오탐이 난다.
|
||||
|
||||
## 같은 자리에서 고칠 수 있다
|
||||
|
||||
§17.1 의 범위 문법 문제와 같은 방향(fail-closed)이다. `reserved` 수집을 스코프 종류와 무관하게 먼저 시도한 뒤 나머지 판정을 갈래로 보낸다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
열거형 reserved 오탐을 실행으로 재현하지 않았다. 스코프 분기 코드로 판정했다. 블록 주석 안의 선언이 스캔되는지도 확인하지 않았다.
|
||||
|
||||
<!-- body:end -->
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-observability-f04
|
||||
title: 감사 sink 인터페이스가 사용처에서 다시 선언된다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-observability-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-observability-f04
|
||||
file: ../../../final/evidence/rendered/messaging-observability-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-observability-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-observability.md#L699 이다.
|
||||
module: messaging-observability
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 감사 sink 인터페이스가 사용처에서 다시 선언된다
|
||||
|
||||
MessagingAuditSink.record(MessagingAuditEvent)와 같은 시그니처를 RedriveService:208이 자기 중첩 인터페이스로 선언한다. messaging-admin-runtime은 messaging-observability에 의존할 수 있다(registry 확인).
|
||||
|
||||
## 관계
|
||||
|
||||
- **타입이 문서화한 불변식은 타입이 강제한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
MessagingAuditSink.record(MessagingAuditEvent)와 같은 시그니처를 RedriveService:208이 자기 중첩 인터페이스로 선언한다.
|
||||
|
||||
messaging-admin-runtime은 messaging-observability에 의존할 수 있다(registry 확인).
|
||||
|
||||
## 결론
|
||||
|
||||
MessagingAuditSink.inMemory()가 제공하는 구현을 admin-runtime이 쓸 수 없다.
|
||||
|
||||
그리고 감사 sink의 계약(분리된 보존·접근·무결성 요구)이 문서화된 곳과 실제로 구현되는 곳이 다르다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 두 인터페이스의 시그니처 대조와 registry 의 의존 허용 여부 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-observability.md#L699 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`MessagingAuditSink.record(MessagingAuditEvent)`와 같은 시그니처를 `RedriveService:208`이 자기 중첩 인터페이스로 선언한다. `messaging-admin-runtime`은 `messaging-observability`에 의존할 수 있다(registry 확인).
|
||||
|
||||
## 같은 시그니처가 선언된 두 곳
|
||||
|
||||
:::evidence key="messaging-observability-f04" alt="분석 문서 analysis/messaging/messaging-observability.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-observability.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 두 결과
|
||||
|
||||
`MessagingAuditSink.inMemory()`가 제공하는 구현을 admin-runtime이 쓸 수 없다. 그리고 감사 sink의 계약(분리된 보존·접근·무결성 요구)이 문서화된 곳과 실제로 구현되는 곳이 다르다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
레닥션이 빠진 감사 이벤트가 실제로 기록되는 것을 관측하지 않았다. 시그니처 대조와 의존 선언으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-runtime-core-f03
|
||||
title: 선언된 content type과 실제 인코딩이 조용히 갈라질 수 있다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-runtime-core-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-runtime-core-f03
|
||||
file: ../../../final/evidence/rendered/messaging-runtime-core-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-runtime-core-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-runtime-core.md#L727 이다.
|
||||
module: messaging-runtime-core
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 선언된 content type과 실제 인코딩이 조용히 갈라질 수 있다
|
||||
|
||||
encode가 codecs.find(message.contentType()).orElseGet(codecs::defaultCodec)으로 폴백한다. 출하 registry에는 JSON codec 하나만 등록된다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **만들어 두고 흘리지 않는 진단값은 진단이 아니다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
encode가 codecs.find(message.contentType()).orElseGet(codecs::defaultCodec)으로 폴백한다.
|
||||
|
||||
출하 registry에는 JSON codec 하나만 등록된다.
|
||||
|
||||
## 결론
|
||||
|
||||
봉투가 application/avro를 선언해도 JSON으로 인코딩되고, EncodedMessage의 content type은 codec이 정하므로 application/json이 된다.
|
||||
|
||||
실패하지 않고 다른 포맷으로 성공한다.
|
||||
|
||||
소비 측이 봉투의 원래 선언을 믿고 디코더를 고르면 어긋난다.
|
||||
|
||||
DestinationProfile.schema().codec()이 목적지의 codec을 선언하는데 그 값과 대조하는 코드가 이 경로에 없다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : encode 의 폴백 코드와 출하 registry 에 등록되는 codec 목록 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-runtime-core.md#L727 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`encode`가 `codecs.find(message.contentType()).orElseGet(codecs::defaultCodec)`으로 폴백한다. 출하 registry에는 JSON codec 하나만 등록된다.
|
||||
|
||||
## encode 의 폴백과 출하 registry
|
||||
|
||||
:::evidence key="messaging-runtime-core-f03" alt="분석 문서 analysis/messaging/messaging-runtime-core.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-runtime-core.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 실패하지 않고 다른 포맷으로 성공한다
|
||||
|
||||
봉투가 `application/avro`를 선언해도 JSON으로 인코딩되고, `EncodedMessage`의 content type은 codec이 정하므로 `application/json`이 된다.
|
||||
|
||||
## 소비 측이 원래 선언을 믿으면 어긋난다
|
||||
|
||||
`DestinationProfile.schema().codec()`이 목적지의 codec을 선언하는데 그 값과 대조하는 코드가 이 경로에 없다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제 브로커로 선언과 다른 포맷이 나가는 것을 관측하지 않았다. 폴백 코드와 등록 목록의 대조로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-schema-avro-f02
|
||||
title: 진화 판단이 두 곳에 있고 형태가 반대다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-schema-avro-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-avro-f02
|
||||
file: ../../../final/evidence/rendered/messaging-schema-avro-f02.svg
|
||||
- key: messaging-schema-avro-f02-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-schema-avro-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-avro-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-avro.md#L533 이다.
|
||||
module: messaging-schema-avro
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 진화 판단이 두 곳에 있고 형태가 반대다
|
||||
|
||||
isTransitive는 SchemaCompatibilityValidator(public static)와 이 leaf(private static)에 글자까지 같은 복사본이 있다. 방향 판정은 전자가 허용목록, 후자가 거부목록이다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
isTransitive는 SchemaCompatibilityValidator(public static)와 이 leaf(private static)에 글자까지 같은 복사본이 있다.
|
||||
|
||||
방향 판정은 전자가 허용목록, 후자가 거부목록이다.
|
||||
|
||||
## 결론
|
||||
|
||||
오늘 7개 모드에서 결과는 같지만 형태가 반대이므로 SchemaCompatibility에 값이 추가되는 순간 갈라진다 — 허용목록은 "검사 안 함", 거부목록은 "양방향 검사".
|
||||
|
||||
그리고 이 중복은 schema-api의 javadoc이 명시적으로 막으려던 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : SchemaCompatibilityValidator 참조 11건 검색과 두 구현의 판정 방향(허용목록·거부목록) 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-schema-avro.md#L533 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`isTransitive`는 `SchemaCompatibilityValidator`(public static)와 이 leaf(private static)에 글자까지 같은 복사본이 있다.
|
||||
|
||||
## 판정이 갈리는 자리
|
||||
|
||||
:::evidence key="messaging-schema-avro-f02-diagram" alt="포트 판정 쪽에 isTransitive 복사본과 허용목록 방향이 놓이고 게이트 판정 쪽에 같은 복사본과 거부목록 방향이 빗금으로 놓인다" caption="판정이 갈리는 자리" zoom="false"
|
||||
:::
|
||||
|
||||
방향 판정은 전자가 허용목록, 후자가 거부목록이다.
|
||||
|
||||
## SchemaCompatibilityValidator 참조 위치
|
||||
|
||||
:::evidence key="messaging-schema-avro-f02" alt="코드베이스에서 SchemaCompatibilityValidator 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SchemaCompatibilityValidator 코드베이스 검색 — 11줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 값이 추가되는 순간 갈라진다
|
||||
|
||||
오늘 7개 모드에서 결과는 같지만 형태가 반대이므로 `SchemaCompatibility`에 값이 추가되면 허용목록은 "검사 안 함", 거부목록은 "양방향 검사"가 된다. 그리고 이 중복은 schema-api의 javadoc이 명시적으로 막으려던 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제 Avro 스키마 진화 사례에서 두 판정이 갈리는지 확인하지 않았다. 테스트는 defaulted 필드 추가·미추가 두 경우만 본다.
|
||||
|
||||
<!-- body:end -->
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-schema-json-f01
|
||||
title: 포맷 중립 payload 정책이, 자기 상수를 두고 JSON codec의 상수를 참조한다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-schema-json-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-json-f01
|
||||
file: ../../../final/evidence/rendered/messaging-schema-json-f01.svg
|
||||
- key: messaging-schema-json-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-schema-json-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-json-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-json.md#L463 이다.
|
||||
module: messaging-schema-json
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 포맷 중립 payload 정책이, 자기 상수를 두고 JSON codec의 상수를 참조한다
|
||||
|
||||
MessagingCoreAutoConfiguration:410-413이 new PayloadPolicy(JacksonMessageCodec.DEFAULT_MAX_BYTES, JacksonMessageCodec.DEFAULT_MAX_BYTES / 2)를 만든다. 그런데 PayloadPolicy 자신이 같은 값의 public 상수 PayloadPolicy.DEFAULT_MAX_BYTES(messaging-policy/PayloadPolicy.java:17)를 갖고 있다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
MessagingCoreAutoConfiguration:410-413이 new PayloadPolicy(JacksonMessageCodec.DEFAULT_MAX_BYTES, JacksonMessageCodec.DEFAULT_MAX_BYTES / 2)를 만든다.
|
||||
|
||||
그런데 PayloadPolicy 자신이 같은 값의 public 상수 PayloadPolicy.DEFAULT_MAX_BYTES(messaging-policy/PayloadPolicy.java:17)를 갖고 있다.
|
||||
|
||||
## 결론
|
||||
|
||||
MessagingAdmissionController는 목적지의 codec이 무엇이든 지나는 관문이다.
|
||||
|
||||
그 상한이 한 포맷 클래스의 상수에서 나오면 두 가지가 깨진다.
|
||||
|
||||
(1) @ConditionalOnMissingBean이 허용하는 대로 애플리케이션이 자기 MessageCodecRegistry를 내놓아 JSON codec을 대체해도, 정책은 여전히 JSON codec의 값을 읽는다.
|
||||
|
||||
(2) 다섯 곳의 리터럴 중 하나만 바뀌면 조용히 갈라지고, RawBytesMessageCodec javadoc이 이미 "shared with the Stable codecs"라고 사실과 다르게 부르고 있다.
|
||||
|
||||
정책 소유자가 이미 존재하는데 배선이 그것을 지나쳤다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : PayloadPolicy 참조 25건 검색과 정책이 읽는 상수의 소유 클래스 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-schema-json.md#L463 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`MessagingCoreAutoConfiguration:410-413`이 `new PayloadPolicy(JacksonMessageCodec.DEFAULT_MAX_BYTES, JacksonMessageCodec.DEFAULT_MAX_BYTES / 2)`를 만든다. 그런데 `PayloadPolicy` 자신이 같은 값의 public 상수 `PayloadPolicy.DEFAULT_MAX_BYTES`(`messaging-policy/PayloadPolicy.java:17`)를 갖고 있다.
|
||||
|
||||
## 참조가 뒤집힌 자리
|
||||
|
||||
:::evidence key="messaging-schema-json-f01-diagram" alt="JacksonMessageCodec 상수만 정책이 읽는 상수 안에 놓이고 PayloadPolicy 자기 상수가 바깥에 빗금으로 놓인다" caption="참조가 뒤집힌 자리" zoom="false"
|
||||
:::
|
||||
|
||||
`MessagingAdmissionController`는 목적지의 codec이 무엇이든 지나는 관문이다.
|
||||
|
||||
## PayloadPolicy 참조 위치
|
||||
|
||||
:::evidence key="messaging-schema-json-f01" alt="코드베이스에서 PayloadPolicy 를 검색한 출력 25줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PayloadPolicy 코드베이스 검색 — 25줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 한 포맷 클래스의 상수에서 나오면 둘이 깨진다
|
||||
|
||||
(1) `@ConditionalOnMissingBean`이 허용하는 대로 애플리케이션이 자기 `MessageCodecRegistry`를 내놓아 JSON codec을 대체해도, 정책은 여전히 JSON codec의 값을 읽는다. (2) 다섯 곳의 리터럴 중 하나만 바뀌면 조용히 갈라지고, `RawBytesMessageCodec` javadoc이 이미 "shared with the Stable codecs"라고 사실과 다르게 부르고 있다. 정책 소유자가 이미 존재하는데 배선이 그것을 지나쳤다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제 배포에서 MessageContracts bean 이 채워지는지 확인하지 못했다. 그 답은 starter 리프가 소유한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-schema-protobuf-f04
|
||||
title: registry 조회 로직이 세 codec에 복제돼 있다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-schema-protobuf-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-protobuf-f04
|
||||
file: ../../../final/evidence/rendered/messaging-schema-protobuf-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-protobuf-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-protobuf.md#L554 이다.
|
||||
module: messaging-schema-protobuf
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# registry 조회 로직이 세 codec에 복제돼 있다
|
||||
|
||||
requireRegistered(JSON/Protobuf)와 schemaFor(Avro)가 같은 3단 판단을 각자 구현한다. JSON과 Protobuf는 registeredVersions 헬퍼까지 사실상 동일하다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
requireRegistered(JSON/Protobuf)와 schemaFor(Avro)가 같은 3단 판단을 각자 구현한다.
|
||||
|
||||
JSON과 Protobuf는 registeredVersions 헬퍼까지 사실상 동일하다.
|
||||
|
||||
## 결론
|
||||
|
||||
판단은 MessageContractKey의 성질이지 포맷의 성질이 아니다.
|
||||
|
||||
그리고 실제로 갈라졌다 — Avro만 AVRO_ 접두 코드를 쓰고 등록 버전 목록을 메시지에 넣지 않는다.
|
||||
|
||||
messaging-schema-api가 흡수할 수 있는 형태다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : MessageContractKey 참조 34건 검색과 세 codec 의 조회 메서드 본문 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-schema-protobuf.md#L554 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`requireRegistered`(JSON/Protobuf)와 `schemaFor`(Avro)가 같은 3단 판단을 각자 구현한다. JSON과 Protobuf는 `registeredVersions` 헬퍼까지 사실상 동일하다.
|
||||
|
||||
## MessageContractKey 참조 위치
|
||||
|
||||
:::evidence key="messaging-schema-protobuf-f04" alt="코드베이스에서 MessageContractKey 를 검색한 출력 34줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageContractKey 코드베이스 검색 — 34줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 판단은 키의 성질이지 포맷의 성질이 아니다
|
||||
|
||||
그리고 실제로 갈라졌다 — Avro만 `AVRO_` 접두 코드를 쓰고 등록 버전 목록을 메시지에 넣지 않는다. `messaging-schema-api`가 흡수할 수 있는 형태다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
파생 프로젝트가 이 codec 을 쓰는지 확인할 수 없었다. 세 구현의 코드 대조로만 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
---
|
||||
kind: QUESTION
|
||||
slug: messaging-cloudevents-f04
|
||||
title: dataschema가 채워질 경로가 없다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: open-question:messaging-cloudevents-f04
|
||||
questionStatus: OPEN
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-cloudevents.md#L547
|
||||
---
|
||||
|
||||
# dataschema가 채워질 경로가 없다
|
||||
|
||||
`dataschema` 를 채우는 코드는 있고 그 값을 만들어 주는 경로가 없다. 이 프로파일이 만드는 CloudEvent 는 스키마 위치를 알리지 않는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 사실
|
||||
|
||||
toCloudEvent 가 encoded.schemaReference().flatMap(SchemaReference::schemaUri).ifPresent(builder::withDataSchema) 로 dataschema 를 채운다.
|
||||
|
||||
세 codec(JSON · Avro · Protobuf)이 모두 SchemaReference.of(subject, version) 로 참조를 만들고, 그 factory 는 schemaUri 를 Optional.empty() 로 둔다.
|
||||
|
||||
dataschema 는 CloudEvents 소비자가 페이로드를 해석하는 데 쓰는 표준 속성이다. schemaversion 확장이 그 자리를 대신하지만 그것은 비표준 확장이다.
|
||||
|
||||
## 미지수
|
||||
|
||||
이 저장소가 외부 schema registry 를 쓸 것인가. messaging-schema-api 의 SchemaRegistry port 가 구현 0 인 것과 같은 뿌리다.
|
||||
|
||||
## 선택지
|
||||
|
||||
registry URI 를 갖는 배포에서 3인자 생성자를 쓴다
|
||||
dataschema 가 채워지고 표준 속성이 제 역할을 한다.
|
||||
|
||||
현재 도달 불가임을 주석으로 남긴다
|
||||
분기를 지우지 않고 그것이 실행되지 않는 이유를 코드 옆에 둔다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
git grep -n 'new SchemaReference(' -- 'src/messaging/**/*.java' 로 3인자 생성자를 부르는 production 코드가 있는지 확인한다.
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
kind: QUESTION
|
||||
slug: messaging-schema-protobuf-f03
|
||||
title: protobuf-java 버전이 저장소에 셋이고 전역 정책이 없다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: open-question:messaging-schema-protobuf-f03
|
||||
questionStatus: OPEN
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-schema-protobuf.md#L545
|
||||
---
|
||||
|
||||
# protobuf-java 버전이 저장소에 셋이고 전역 정책이 없다
|
||||
|
||||
세 버전이 오늘 한 classpath 를 공유하지 않아 사고가 아니다. 이 leaf 를 런타임에 편입하는 순간 버전 판정이 필요해지고, 그때 참조할 전역 정책이 없다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 사실
|
||||
|
||||
ext.protobufVersion = 3.25.5(grpc 모듈 범위로 한정), 이 leaf 4.29.3, websocket 4.33.2. lockfile 들이 세 값을 모두 고정한다.
|
||||
|
||||
근거 위치는 src/build.gradle:174-180 · messaging-schema-protobuf/build.gradle:9 · adapter/inbound/websocket/build.gradle:44,46 와 각 gradle.lockfile 이다.
|
||||
|
||||
이 leaf 의 runtime_memberships 가 [] 이라 세 버전이 한 classpath 를 공유하지 않는다. 채택 시점의 부채다.
|
||||
|
||||
src/build.gradle 의 "the single SSOT" 라는 표현이 전역 정책의 존재를 시사하는데, 실제 범위는 그 문장 안에서 grpc 모듈로 한정된다.
|
||||
|
||||
## 미지수
|
||||
|
||||
이 leaf 를 런타임에 편입할 것인가. 저장소 안에 답이 없다.
|
||||
|
||||
## 선택지
|
||||
|
||||
편입 전까지 현 상태를 유지한다
|
||||
src/messaging/CLAUDE.md 에 "편입 시 버전 정합을 먼저 판정한다" 를 적어 판정 시점을 예약한다.
|
||||
|
||||
ext.protobufVersion 의 범위를 넓힌다
|
||||
주석의 "single SSOT" 표현이 실제 범위와 맞아진다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
git grep -n 'protobuf-java\|protobufVersion' -- src --include='*.gradle' 로 세 값은 다시 확정된다. 편입 여부의 결정은 그 검색으로 답해지지 않는다.
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-schema-avro-f03
|
||||
title: 컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-schema-avro-f03
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-schema-avro.md#L542
|
||||
---
|
||||
|
||||
# 컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다
|
||||
|
||||
## 관계
|
||||
|
||||
- **진화 판단이 두 곳에 있고 형태가 반대다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **CI에서 돈다고 선언한 게이트를 부르는 CI가 없다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
둘을 잇는 코드가 없어 지금은 무해하다. 이으면서 reversed() 를 빠뜨리면 pairwise 모드가 가장 오래된 스키마를 직전 버전으로 비교한다. 실패하지 않고 통과할 수 있는 오류라는 점이 이 규칙의 이유다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 같은 리스트를 주고받는 두 쪽의 순서 문장을 대조한다
|
||||
SchemaRegistry.history javadoc 은 oldest first, AvroCompatibilityGate.check 의 @param history 는 newest first 다.
|
||||
|
||||
2. 위험이 이미 문서화돼 있는지 본다
|
||||
port javadoc 이 그 위험을 직접 적는다 — "an ordering mistake here silently converts a transitive check into a pairwise one."
|
||||
|
||||
3. 한 방향으로 통일하고 뒤집기를 안쪽으로 넣는다
|
||||
게이트가 oldest-first 를 받고 내부에서 뒤집는 형태다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
정렬된 컬렉션이 port 와 그 소비자 사이를 오가고, 순서가 결과를 바꾸는 모든 경계.
|
||||
|
||||
## 예외
|
||||
|
||||
SSOT 가 이 규칙의 반례를 적지 않았다. 두 방향이 의도적이라면 변환 지점이 코드로 존재해야 하는데, 지금은 잇는 코드 자체가 없다.
|
||||
|
||||
## 예시
|
||||
|
||||
두 javadoc 의 순서 문장. 확인 방법은 그 둘을 나란히 대조하는 것이다.
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-schema-avro-f05
|
||||
title: 안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다
|
||||
topic: schema-and-data-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-schema-avro-f05
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-schema-avro.md#L560
|
||||
---
|
||||
|
||||
# 안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다
|
||||
|
||||
## 관계
|
||||
|
||||
- **진화 판단이 두 곳에 있고 형태가 반대다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **CI에서 돈다고 선언한 게이트를 부르는 CI가 없다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
FailureDescriptor.code 는 "stable, machine-readable code" 이고 대시보드와 재시도 정책이 그것으로 집계한다. 같은 판단이 두 어휘로 나뉘면 Avro 만 별도 계열이 되고, 집계는 포맷 수만큼 갈라진다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 같은 판단에 형제 구현들이 어떤 코드를 쓰는지 모은다
|
||||
미등록 타입과 미등록 버전이라는 같은 판단에 JSON 과 Protobuf 는 UNKNOWN_MESSAGE_TYPE 과 SCHEMA_VERSION_NOT_REGISTERED 를, Avro 는 AVRO_TYPE_NOT_REGISTERED 와 AVRO_VERSION_NOT_REGISTERED 를 쓴다.
|
||||
|
||||
2. 코드가 갈라진 이유가 판단인지 구현인지 묻는다
|
||||
포맷 이름이 접두로 붙은 것은 구현 단위다.
|
||||
|
||||
3. 포맷 구분은 코드가 아니라 메시지로 옮긴다
|
||||
공통 코드를 쓰고 sanitizedMessage 로 포맷을 구분한다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
여러 구현이 같은 port 를 구현하면서 각자 실패 코드를 정하는 모든 가족.
|
||||
|
||||
## 예외
|
||||
|
||||
판단 자체가 그 구현에만 존재하는 경우는 고유 코드가 맞다. 여기 두 판단은 세 codec 모두에 있다.
|
||||
|
||||
## 예시
|
||||
|
||||
세 codec 의 requireRegistered 와 schemaFor. 확인 방법은 git grep -n 'NOT_REGISTERED' -- 'src/messaging/**/*.java' 다.
|
||||
|
||||
Reference in New Issue
Block a user