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
+163
@@ -0,0 +1,163 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: a05-f007-transactionprofileregistry
|
||||
title: 오타를 막는 근거만 남기고, 오타가 들어올 자리를 지웠다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:a05-f007-transactionprofileregistry
|
||||
evidenceCapturedOn: 2026-09-02
|
||||
assets:
|
||||
- key: a05-f007-transactionprofileregistry-adr
|
||||
file: ../../../final/evidence/rendered/a05-f007-transactionprofileregistry-adr.svg
|
||||
- key: a05-f007-transactionprofileregistry
|
||||
file: ../../../final/evidence/rendered/a05-f007-transactionprofileregistry.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/a05-f007-transactionprofileregistry-adr.txt
|
||||
- ../../../final/evidence/raw/a05-f007-transactionprofileregistry.txt
|
||||
source:
|
||||
- 원본 분석 절은 `analysis/05-adapter-outbound-persistence-jpa.md` §25 다. 프로덕션 참조 0, 전용 단위 테스트만 존재, 비공개 API 패키지, 루트 빈 배선 없음이 그 절의 판정이고 등급은 P3 다. ADR 대조와 형제 레지스트리 비교는 이 기록에서 새로 확인했다. 바로 앞 §24 는 같은 리프에서 트랜잭션 스택이 둘로 갈린 문제를 다룬다.
|
||||
---
|
||||
|
||||
# 오타를 막는 근거만 남기고, 오타가 들어올 자리를 지웠다
|
||||
|
||||
프로파일을 이름으로 찾고 등록되지 않은 이름을 거절하는 레지스트리가 남았다. 이름을 건네던 유일한 호출부는 한 커밋에서 지워졌고, 같은 커밋이 그 레지스트리의 javadoc 에서 지워진 애너테이션 이름만 빼고 오타 예시와 논거는 남겼다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **재시도 구현이 둘이고 정교한 쪽을 아무도 호출하지 않는다**
|
||||
같은 스택의 다른 마디가 같은 형태로 남은 사례다.
|
||||
- **legacy compatibility surface의 제거 조건을 세 가지로 고정한다**
|
||||
이 후보를 그 세 조건에 대 봤다.
|
||||
- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다**
|
||||
같은 개념을 가리키는 어휘가 둘 남았을 때 어느 쪽이 죽었는지 표시하라는 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
이 레지스트리는 등록되지 않은 이름에 기본값을 주지 않고 던진다. javadoc 이 그 이유로 프로파일 이름 오타 하나가 남의 격리 수준과 타임아웃과 재시도 예산을 물려받는 상황을 든다.
|
||||
|
||||
그런 방어가 값을 하려면 이름을 건네는 쪽이 있어야 한다.
|
||||
|
||||
## 결론
|
||||
|
||||
이름을 건네던 곳은 하나였다. 삭제된 인터셉터의 66행이 애너테이션에 적힌 프로파일 이름을 레지스트리에 넘기고, 69행이 그렇게 얻은 프로파일로 코디네이터를 불렀다.
|
||||
|
||||
애너테이션 44줄, 인터셉터 109줄, 그 테스트 211줄이 한 커밋에서 함께 지워졌다. 파일 909개를 옮긴 기능 커밋 하나에 이 삭제가 섞여 들어갔다.
|
||||
|
||||
같은 커밋이 레지스트리 javadoc 도 한 줄 고쳤다. 오타 예시에서 애너테이션 표기를 빼고 이름만 남겼다. 방어의 근거는 손질해서 남기고, 방어할 대상은 같은 커밋으로 지운 것이다.
|
||||
|
||||
지금 세면 자기 파일을 뺀 프로덕션 참조 0, 자동설정 등록 0, 설정 키 0 이다. 패키지는 스캔 범위에 들어가는데 이 클래스에는 스테레오타입 애너테이션이 없다. 남은 참조는 전용 단위 테스트 한 파일의 셋이다.
|
||||
|
||||
레지스트리가 담는 타입도 사정이 다르지 않다. TransactionProfile 은 네 파일에 나타나고 공개 포트가 그것을 두 번째 인자로 받는다. 그런데 그 포트를 참조하는 main 파일은 포트 자신과 구현체뿐이고, 자기 파일 밖에서 프로파일을 만드는 main 코드도 없다. 참조가 네 파일 안에서만 돌고 바깥에서 들어오는 길이 없다.
|
||||
|
||||
프로덕션 트랜잭션은 다른 타입으로 열린다. 같은 패키지의 SpringTransactionPort 는 애플리케이션 계층 요청 타입을 받는데, 그 파일에서 TransactionProfile 을 부르는 곳이 없다.
|
||||
|
||||
그래서 이름 조회가 객체 전달로 대체된 것으로 읽으면 틀린다. 이름을 받던 입력이 사라졌고, 객체를 받는 쪽에도 프로덕션 호출자가 붙지 않았다. 레지스트리는 그 스택에서 가장 먼저 잘려 나간 마디다.
|
||||
|
||||
제거 조건 셋 중 둘은 확인된다. 외부 프로덕션 참조가 0 이고 설정 경로가 없다. 세 번째인 대체 경로의 특성화는 확인되지 않는다. 대체 경로가 같은 동작을 다르게 하는 것이 아니라, 이름 조회를 수행하는 프로덕션 경로가 없다.
|
||||
|
||||
이런 모양 자체가 버려진 것은 아니다. 문자열 이름을 받아 등록되지 않았으면 던지는 레지스트리를 정렬 필드 매퍼가 지금도 호출한다. 형제로 셀 수 있는 것은 결국 하나다. gRPC 채널 레지스트리는 검증된 값 객체를 키로 쓰고, Mongo 일관성 레지스트리는 enum 을 키로 써서 조회가 실패할 수 없다고 javadoc 이 적는다.
|
||||
|
||||
ADR 은 아직 지워진 것을 전제한다. 강제 절이 삭제된 클래스의 상수를 근거로 지목하고, 결정 절은 재시도 어드바이스가 스프링 트랜잭션 어드바이스 바깥에 정렬된다고 적는다. 그 상수 이름은 소스 전체에 없고, JPA 리프 main 에 어드바이저도 없다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
확인 방식 : 단어 경계 참조 계수, 삭제 커밋의 부모와 diff 열람, 형제 레지스트리의 키 타입 대조, ADR 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 레지스트리의 패키지와 이 리프의 공개 API 패키지를 비교한다.
|
||||
2. 자기 파일을 뺀 프로덕션 참조와 자동설정 등록과 설정 키를 센다. 스캔 대상 패키지인지, 스테레오타입이 있는지도 본다.
|
||||
3. 레지스트리가 담는 타입의 참조를 세고, 그 타입을 만드는 코드와 포트를 부르는 코드를 따로 센다.
|
||||
4. 프로덕션 트랜잭션이 실제로 지나는 포트를 찾아 그 타입을 확인한다.
|
||||
5. 삭제 커밋의 부모에서 인터셉터를 열고, 같은 커밋의 레지스트리 diff 를 본다.
|
||||
6. 형제 레지스트리들의 require 키 타입을 읽는다.
|
||||
7. ADR 의 결정 절과 강제 절이 지목한 것을 소스에서 찾는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`TransactionProfileRegistry` 는 이름을 받아 트랜잭션 프로파일을 돌려주고, 등록되지 않은 이름이면 기본값으로 넘어가는 대신 예외를 던진다.
|
||||
|
||||
클래스의 javadoc 이 그 완고함의 근거를 적는다. 기본값이 있으면 `"order-wrtie"` 같은 오타가 남의 격리 수준과 타임아웃과 재시도 예산으로 조용히 실행된다는 것이다.
|
||||
|
||||
그런 방어는 이름을 건네는 쪽이 있을 때만 값을 한다.
|
||||
|
||||
## 이름을 건네던 코드는 한 번 있었고, 지워졌다
|
||||
|
||||
:::evidence key="a05-f007-transactionprofileregistry-adr" alt="삭제 커밋의 규모와 지워진 세 파일, 그 커밋의 부모에서 열어 본 인터셉터의 해당 줄, 같은 커밋이 레지스트리 javadoc 에 낸 diff, 문자열 키를 쓰는 형제 레지스트리와 다른 키 타입을 쓰는 둘, 그리고 ADR 의 결정 절과 강제 절 원문을 차례로 출력한 터미널 기록." caption="삭제 규모와 지워진 세 파일 · 삭제 직전의 호출부 두 줄 · 같은 커밋의 javadoc diff · 문자열 키 형제는 하나 · ADR 두 절이 지목한 것 — 43줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
한 커밋이 애너테이션 44줄과 인터셉터 109줄과 그 테스트 211줄을 지웠다. 909개 파일을 손댄 큰 기능 커밋이고, 이 삭제는 그 안에 있다.
|
||||
|
||||
지운 파일을 그 커밋의 부모에서 열면 레지스트리를 어떻게 썼는지 보인다.
|
||||
|
||||
```java
|
||||
41: private final TransactionProfileRegistry profiles;
|
||||
66: TransactionProfile profile = profiles.require(policy.profile());
|
||||
69: return coordinator.execute(operation, profile, () -> proceed(invocation), transactionKey);
|
||||
```
|
||||
|
||||
`policy.profile()` 은 애너테이션에 적힌 문자열이다. 오타가 들어올 수 있는 자리가 거기였다.
|
||||
|
||||
## 같은 커밋이 근거만 손질해서 남겼다
|
||||
|
||||
```text
|
||||
- * default would mean a typo in {@code @RetryableJpaTransaction(profile = "order-wrtie")} silently
|
||||
+ * default would mean a typo in a profile name of {@code "order-wrtie"} silently runs with someone
|
||||
```
|
||||
|
||||
지워지는 애너테이션의 이름만 문장에서 빼고, 오타 예시와 논거는 그대로 뒀다. 방어할 대상을 지우는 커밋이 방어의 근거는 문법만 고쳐 남긴 것이다.
|
||||
|
||||
## 이름이 사라진 자리를 객체가 넘겨받지도 않았다
|
||||
|
||||
:::evidence key="a05-f007-transactionprofileregistry" alt="레지스트리의 패키지 위치와 공개 API 패키지 파일 수, 자기 파일을 제외한 프로덕션 참조와 자동설정 등록과 설정 키의 계수, 컴포넌트 스캔 대상 여부와 스테레오타입 유무, 레지스트리가 담는 타입의 참조 계수와 그 타입을 만드는 코드 수, 포트를 참조하는 파일, 그리고 프로덕션 트랜잭션이 실제로 지나는 포트와 그 타입을 출력한 터미널 기록." caption="구현 패키지 · 프로덕션 참조 0 과 설정 키 0 · 스캔 대상이나 스테레오타입 없음 · 담긴 타입은 네 파일에 있으나 만드는 코드 0 · 실제 경로는 다른 포트 — 37줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
레지스트리 쪽 수는 전부 0 이다. 자기 파일을 뺀 프로덕션 참조 0, 자동설정 등록 0, 출하 설정의 프로파일 키 0. 리프의 공개 API 패키지에는 파일이 마흔아홉 개 있지만 이것은 그중에 없다. 패키지 자체는 컴포넌트 스캔 대상인데, 이 클래스에 스테레오타입 애너테이션이 없어 후보가 되지 않는다.
|
||||
|
||||
레지스트리가 담는 타입 쪽은 수가 다르다. `TransactionProfile` 은 네 파일에서 열한 줄에 나타나고, import 와 javadoc 을 빼면 일곱 줄이 타입 사용이다. 코디네이터, 정의 매퍼, 실행기 구현, 그리고 공개 포트다. 포트는 이름 대신 객체를 받는다.
|
||||
|
||||
```java
|
||||
<T> T execute(PersistenceOperationName operation, TransactionProfile profile, Supplier<T> work);
|
||||
```
|
||||
|
||||
두 번째 인자가 프로파일 객체다. 다만 이 포트를 참조하는 main 파일은 포트 자신과 그 구현체뿐이고, 자기 파일 밖에서 `TransactionProfile` 을 만드는 main 코드도 없다. 네 파일은 서로를 참조할 뿐이고 바깥에서 들어오는 진입점이 없다.
|
||||
|
||||
프로덕션 트랜잭션은 같은 패키지의 다른 클래스가 연다. `SpringTransactionPort` 가 애플리케이션 계층의 요청 타입을 받고, 그 파일 안의 `TransactionProfile` 참조는 0 이다.
|
||||
|
||||
그러니 이름 조회가 객체 전달로 대체됐다고는 말할 수 없다. 이름을 받던 입력이 사라진 뒤 객체를 받는 쪽에도 호출자가 붙지 않았고, 레지스트리는 그 스택에서 가장 먼저 잘려 나간 마디다.
|
||||
|
||||
## 제거 조건 세 가지에 대 보면
|
||||
|
||||
두 조건은 확인된다. 외부 프로덕션 참조가 0 이고, 운영자가 켤 수 있는 설정 경로가 없다.
|
||||
|
||||
세 번째 조건인 대체 경로의 특성화는 확인되지 않는데, 이유가 규칙이 상정한 것과 다르다. 규칙은 대체 경로가 같은 동작을 증명하지 못하면 지우는 순간 동작이 바뀐다고 말한다. 여기서는 이름 조회를 수행하는 프로덕션 경로가 없다. 지워도 바뀔 동작이 없는 대신, 무엇이 대체했는지도 말할 수 없다.
|
||||
|
||||
이 모양이 폐기된 것도 아니다. `SafeSortRegistry` 는 지금도 문자열 이름을 받아 등록되지 않았으면 던지고, 정렬 파라미터를 파싱하는 매퍼가 그것을 호출한다.
|
||||
|
||||
형제라 부를 만한 것은 그 하나다. gRPC 채널 레지스트리는 검증된 값 객체를 키로 쓰고, 던지는 이유도 오타가 아니라 아직 설치되지 않았다는 것이다. Mongo 일관성 레지스트리는 enum 을 키로 쓰고, 그 javadoc 이 모든 enum 상수가 언제나 있다고 적는다. 컴파일러가 키를 강제하므로 오타가 들어올 자리가 없다.
|
||||
|
||||
## ADR 이 지워진 것을 아직 전제한다
|
||||
|
||||
전체 트랜잭션 재시도 ADR 의 Enforcement 절이 두 가지를 근거로 지목한다.
|
||||
|
||||
```text
|
||||
`FullTransactionRetryCoordinatorTest`; `RetryableJpaTransactionInterceptor.DEFAULT_ORDER`.
|
||||
```
|
||||
|
||||
앞의 것은 있다. 뒤의 것은 삭제된 클래스의 상수이고, `DEFAULT_ORDER` 라는 이름은 소스 전체에 한 줄도 없다.
|
||||
|
||||
Decision 절도 마찬가지다. 재시도 어드바이스가 스프링 트랜잭션 어드바이스 바깥에 정렬되어 각 시도가 새 트랜잭션을 연다고 적는데, JPA 리프 main 에 어드바이저는 없다. 코드는 정리됐고 결정 기록은 그대로 남았다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
저장소 밖의 리플렉션이나 외부 직접 생성 여부는 소스 검색으로 알 수 없다.
|
||||
|
||||
포크가 이름 기반 조회를 다시 붙일 의도인지도 알 수 없다. 테스트 머리글이 계획 문서의 과제 번호와 설계 절을 가리키지만, 그 계획이 지금도 유효한지는 이 저장소가 말하지 않는다.
|
||||
|
||||
<!-- body:end -->
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: a05-f018-sql
|
||||
title: 쿼리 이름을 SQL 로 옮기는 두 경로가 양쪽 끝만 있고 가운데가 없다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:a05-f018-sql
|
||||
evidenceCapturedOn: 2026-09-02
|
||||
assets:
|
||||
- key: a05-f018-sql
|
||||
file: ../../../final/evidence/rendered/a05-f018-sql.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/a05-f018-sql.txt
|
||||
source:
|
||||
- 원본 분석 절은 `analysis/05-adapter-outbound-persistence-jpa.md` §55 의 backlog 안, 「Cross-scope P1/P2 — query SQL naming/observability composition 부재」 항목이다. 검사기의 런타임 등록이 0 이라는 판정, SQL 계층으로 잇는 다리가 확인되지 않았다는 판정, 그리고 최종 판정을 트랜잭션 관측 배선 공백과 함께 관측·설정 범위로 미룬다는 기록이 거기 있다.
|
||||
- 다리가 확인되지 않았다는 판정은 맞다. `QueryNameContext` 가 이름을 스레드에 묶고 검사기가 그것을 읽지만, 어떤 SessionFactory 도 그 검사기를 설치하지 않으므로 `inspect()` 는 호출되지 않는다. 코드에 있는 것은 다리가 아니라 양쪽 끝이다. 두 번째 경로인 힌트도 마찬가지다.
|
||||
---
|
||||
|
||||
# 쿼리 이름을 SQL 로 옮기는 두 경로가 양쪽 끝만 있고 가운데가 없다
|
||||
|
||||
쿼리 이름을 SQL 주석으로 실어 나르는 문장 검사기가 있다. 어떤 SessionFactory 도 그것을 설치하지 않아 `inspect()` 가 호출되지 않는다. 두 번째 경로인 `org.hibernate.comment` 힌트도 그것을 SQL 로 내보내는 설정이 꺼져 있다. 이름을 넣는 프로덕션 클래스 셋은 자기 단위 테스트 밖에서 쓰이지 않는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **@Bean이 있다는 것은 조립 증거가 아니다**
|
||||
구현의 존재를 조립의 증거로 읽는다는 점이 같다.
|
||||
- **이름은 값이 아니라 registry key다**
|
||||
쿼리 이름이 등록된 식별자로 쓰이는 맥락이다.
|
||||
- **카디널리티 경계를 타입으로 표현하기**
|
||||
그 이름이 메트릭 태그가 되는 경계다.
|
||||
|
||||
## 문제
|
||||
|
||||
주석에 이름이 실려야 DBA 가 눈앞의 문장을 어느 유스케이스에서 나온 것인지 되짚는다. 검사기 javadoc 이 그 이유를 적고, 없으면 어느 엔드포인트가 이 쿼리를 내는지를 SQL 조각으로 코드베이스를 뒤져 답하게 된다고 덧붙인다.
|
||||
|
||||
## 결론
|
||||
|
||||
inspect() 는 다 쓰여 있다. 현재 스레드에 묶인 이름을 꺼내 /* 이름 */ 을 앞에 붙이고, 이름에 주석 종료자가 있으면 원본을 돌려준다. 그 분기는 도달하지 않는다. QueryName 의 형식 [a-z][a-z0-9.-]{2,95} 가 이미 * 와 / 를 막고, javadoc 도 그 검사를 additionally 라고 적는다.
|
||||
|
||||
단위 테스트는 없다. 검사기에도 컨텍스트에도 테스트 파일이 없다.
|
||||
|
||||
SessionFactory 가 설치해 주지 않으면 inspect() 는 불리지 않는다. 네 경로는 저장소 루트에서 확장자 제한 없이 훑어 얻었다. 설정 키 0, HibernatePropertiesCustomizer 와 SessionFactoryBuilder 와 ServiceRegistry 와 Integrator 0, persistence.xml 0, 런타임 리소스의 FQCN 0 이다.
|
||||
|
||||
이름을 넣는 쪽은 배선되어 있다. QueryNameContext.with(...) 를 부르는 프로덕션 코드가 셋이다. 그 컨텍스트를 읽는 쪽은 검사기 하나뿐이다.
|
||||
|
||||
SQL 로 이름을 옮기는 길이 그것 말고 또 있다. org.hibernate.comment 힌트를 거는 것은 리포지토리 조각 지원과 Querydsl 지원이다. 그 힌트도 스위치 하나에 달려 있는데 저장소에 그 키가 0 이다.
|
||||
|
||||
두 경로 다 이름을 넣는 코드와 그것을 읽는 자리는 있는데, 그 사이를 잇는 설치와 스위치가 없다.
|
||||
|
||||
넣는 세 클래스도 실행되지 않는다. 각 타입을 쓰는 파일은 자기 자신과 자기 단위 테스트뿐이고, 리포지토리 조각 지원을 상속하는 유일한 코드도 그 테스트 안의 중첩 클래스다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
확인 방식 : 설치 경로 넷을 저장소 루트에서 계수, 이름을 넣고 읽는 지점 추적, 두 번째 경로의 활성 설정 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 검사기 구현과 javadoc, 그리고 QueryName 의 형식 정규식을 읽는다.
|
||||
2. 저장소 루트에서 확장자 제한 없이 설치 경로 넷을 각각 센다.
|
||||
3. 그 클래스의 단위 테스트가 있는지 찾는다.
|
||||
4. 이름을 넣는 호출과 읽는 호출을 각각 센다.
|
||||
5. org.hibernate.comment 힌트를 거는 곳과, use_sql_comments 를 켜는 곳을 센다.
|
||||
6. 넣는 세 클래스를 쓰는 파일과 상속하는 코드를 전부 나열한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
쿼리 이름이 SQL 주석으로 실려야 DBA 가 `pg_stat_activity` 나 느린 쿼리 로그의 문장을 유스케이스로 되돌릴 수 있다. 검사기 javadoc 이 그 이유를 적는다.
|
||||
|
||||
## inspect() 가 하는 일
|
||||
|
||||
:::evidence key="a05-f018-sql" alt="문장 검사기의 javadoc 앞부분과 클래스 본문, 쿼리 이름의 형식 정규식, 하이버네이트가 이 구현을 설치하는 경로 넷을 저장소 루트에서 확장자 제한 없이 센 결과와 그 클래스의 단위 테스트 수, 이름을 컨텍스트에 넣는 세 곳과 읽는 곳, 이름을 SQL 로 옮기는 두 번째 경로와 그것을 활성화하는 설정의 수, 그리고 넣는 세 클래스를 쓰는 파일과 상속하는 코드를 출력한 터미널 기록." caption="검사기 javadoc 과 본문 · 이름 형식이 이미 * 와 / 를 막음 · 설치 경로 넷 전부 0 · 단위 테스트 0 · 넣는 곳 셋과 읽는 곳 하나 · 두 번째 경로의 use_sql_comments 0 · 세 클래스는 자기 테스트만 — 69줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
```java
|
||||
public String inspect(String sql) {
|
||||
if (sql == null) {
|
||||
return null;
|
||||
}
|
||||
Optional<QueryName> queryName = QueryNameContext.current();
|
||||
if (queryName.isEmpty()) {
|
||||
return sql;
|
||||
}
|
||||
String value = queryName.get().value();
|
||||
if (value.contains(COMMENT_TERMINATOR)) {
|
||||
return sql;
|
||||
}
|
||||
return "/* " + value + " */ " + sql;
|
||||
}
|
||||
```
|
||||
|
||||
현재 스레드에 묶인 이름을 꺼내 주석으로 붙인다. 이름에 `*/` 가 있으면 원본을 돌려주는 분기가 하나 더 있는데, `QueryName` 의 형식 `[a-z][a-z0-9.-]{2,95}` 가 이미 `*` 와 `/` 를 막으므로 도달하지 않는다. javadoc 도 그 검사를 additionally 라고 적는다.
|
||||
|
||||
이 클래스에는 단위 테스트가 없다. 컨텍스트 쪽도 없다.
|
||||
|
||||
## 검사기를 설치하는 설정이 없다
|
||||
|
||||
하이버네이트가 `inspect()` 를 부르려면 SessionFactory 가 이 구현을 설치해야 한다. 설치 경로 넷을 저장소 루트에서 확장자 제한 없이 셌다.
|
||||
|
||||
```text
|
||||
statement_inspector 설정 키 : 0
|
||||
HibernatePropertiesCustomizer / SessionFactoryBuilder / Integrator : 0
|
||||
persistence.xml : 0
|
||||
런타임 리소스의 FQCN : 0
|
||||
```
|
||||
|
||||
## 이름을 컨텍스트에 넣는 세 곳
|
||||
|
||||
`JpaStreamExecutor:63`, `JpaKeysetQuerySupport:49`, `JpaRepositoryFragmentSupport:72` 가 작업을 감싸며 이름을 컨텍스트에 넣는다. 그 컨텍스트를 읽는 코드는 검사기 하나다.
|
||||
|
||||
이름을 SQL 로 옮기는 경로는 하나가 더 있다. 리포지토리 조각 지원과 Querydsl 지원이 `org.hibernate.comment` 힌트를 건다.
|
||||
|
||||
```java
|
||||
return entityManager.createQuery(jpql, resultType).setHint(COMMENT_HINT, name.value());
|
||||
```
|
||||
|
||||
그 힌트가 SQL 로 나오려면 `hibernate.use_sql_comments` 가 켜져야 한다. 저장소에 그 키는 0 이다.
|
||||
|
||||
두 경로 모두 양쪽 끝만 있고 가운데가 없다.
|
||||
|
||||
## 세 클래스의 프로덕션 참조
|
||||
|
||||
각 타입을 쓰는 파일은 자기 자신과 자기 단위 테스트뿐이다. 리포지토리 조각 지원을 상속하는 유일한 코드도 그 테스트 안의 중첩 클래스다.
|
||||
|
||||
스프링 데이터의 조각 규약으로 연결될 여지도 없다. 그 클래스는 인터페이스가 아니라 추상 클래스이고, `repositoryBaseClass` 도 `@NoRepositoryBean` 도 저장소에 없다.
|
||||
|
||||
이름은 실행 경로에 오르지 않는다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
애플리케이션을 부팅해 실제 문장에 주석이 붙지 않는 것을 관측하지 않았다. 설치 경로와 호출 지점을 센 것까지가 확인 범위다.
|
||||
|
||||
<!-- body:end -->
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: a06-f003-change-streams-true
|
||||
title: 거부라고 적힌 처리가 폐기이고, 그 값을 읽는 시작 검사는 켤 방법이 없다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:a06-f003-change-streams-true
|
||||
evidenceCapturedOn: 2026-09-02
|
||||
body: case-a06-f003-change-streams-true.body.md
|
||||
assets:
|
||||
- key: a06-f003-change-streams-true
|
||||
file: ../../../final/evidence/rendered/a06-f003-change-streams-true.svg
|
||||
- key: a06-f003-change-streams-true-probe
|
||||
file: ../../../final/evidence/rendered/a06-f003-change-streams-true-probe.svg
|
||||
- key: a06-f003-change-streams-true-shipped
|
||||
file: ../../../final/evidence/rendered/a06-f003-change-streams-true-shipped.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/a06-f003-change-streams-true.txt
|
||||
- ../../../final/evidence/raw/a06-f003-change-streams-true-probe.txt
|
||||
- ../../../final/evidence/raw/a06-f003-change-streams-true-shipped.txt
|
||||
source:
|
||||
- 원본 분석 절은 `analysis/06-adapter-outbound-persistence-mongo.md#L156` 이다.
|
||||
- 그 절의 판정은 P3 이고, 근거로 변경 스트림 실행체가 애초에 출하되지 않는다는 것을 든다. 같은 문서 `#L1069` 이 이 리비전에서 그 전제를 부정한다. 그래서 P3 에 붙은 "현재 잘못된 동작을 만들지는 않는다"는 그대로 두기 어렵지만, 등급 재조정은 이 기록이 하지 않는다. `#L1069` 의 P2 는 별도 기록이 다룬다.
|
||||
- 네 호출이 각각 어디서 멈추는지, 그 분기가 내는 문장이 저장소에 몇 번 나오는지, 소비자 조건 다섯의 구현체가 전부 시험 픽스처라는 것, 그리고 세 입력의 바인딩 결과와 단독 서버에서의 두 배선 비교와 복구 정책 판정은 이 기록에서 확인했다.
|
||||
---
|
||||
|
||||
# 거부라고 적힌 처리가 폐기이고, 그 값을 읽는 시작 검사는 켤 방법이 없다
|
||||
|
||||
설정 타입의 컴팩트 생성자가 변경 스트림 플래그를 예외 없이 거짓으로 덮어쓴다. 그 자리의 주석은 이 처리를 거부라고 부른다. 거부와 폐기는 운영자에게 다른 사건이다. 하나는 기동이 멈추고 하나는 아무 일도 일어나지 않는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **하위 시스템 전체가 미배선인데 그것을 켜는 플래그는 시작 검사를 수행한다**
|
||||
그 기록에서는 플래그가 켜져 있어 요구를 만드는데 실행체가 없다.
|
||||
- **sanitize가 아니라 reject가 기본이다**
|
||||
이 주석이 따르겠다고 선언한 규칙이다.
|
||||
- **플래그는 고정 거짓이라 능력 검사를 끄지만, 조립 조건이 아니라서 소비자 빈은 그대로 생성된다**
|
||||
같은 플래그를 다른 절에서 다룬 기록이다. 등급 재조정은 그 기록이 한다.
|
||||
|
||||
## 문제
|
||||
|
||||
레코드 컴포넌트는 넷이고 생성자는 넷을 서로 다르게 다룬다. 프로파일은 널이면 빈 맵으로 흡수한다. 필요 보조 노드 수는 널이면 기본값을 넣고 음수면 예외로 거부한다. 형제 불리언인 트랜잭션 플래그는 그대로 보존한다. 변경 스트림 플래그만 예외 없이 거짓이 된다.
|
||||
|
||||
## 결론
|
||||
|
||||
이 플래그가 실제로 끄는 것은 실행체가 아니라 검사다. 시작 검증기는 변경 스트림이 켜져 있을 때 토폴로지 능력을 확인하는 분기를 갖는다. 그 인자를 넘기는 프로덕션 지점은 자동 구성 한 줄이고, 그 줄이 읽는 값은 생성자가 이미 거짓으로 덮어쓴 뒤다.
|
||||
|
||||
시험도 그 분기 본문에 닿지 않는다. 검증기를 만드는 시험은 하나이고 호출은 넷인데, 둘은 거짓을 넘기고, 하나는 앞 단계인 토폴로지 검사에서 먼저 멈추며, 나머지 하나는 복제 셋이라 능력이 안정으로 나온다. 그 분기가 내는 문장은 저장소 안에서 자기 throw 자리 한 곳에만 있고, 그것을 단언하는 시험은 없다.
|
||||
|
||||
오플로그가 없는 단독 서버로 확인했다. 출하 배선의 시작 검증은 통과하고, 같은 검증기에 리터럴 참을 넘기면 토폴로지를 지목하는 예외가 난다. 그 뒤 커서를 열면 드라이버가 명령 단계에서 거절하고, 복구 정책은 그것을 실패로 확정하며 프라이머리 장애 조치 런북을 붙인다. 그 런북에 오플로그 없는 토폴로지 항목은 없다.
|
||||
|
||||
주석은 이 처리의 근거로 드라이버 쪽 구현이 출하되지 않아 빈이 0 이라는 것을 든다. 그 근거는 이 리비전에서 성립하지 않는다. 다만 그 사실이 바꾸는 것은 원본 절의 등급 판단이고, 이 기록의 관찰은 거부와 폐기의 차이 그대로다.
|
||||
|
||||
수정은 둘 중 하나다. 값을 정말로 거부하거나, 플래그를 레코드 컴포넌트에서 빼 존재하지 않는 스위치로 만드는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
MongoDB : 8.0.16 단독 서버
|
||||
확인 방식 : 실제 바인딩에 세 입력 통과, 실제 서버에 시작 검증기와 커서 열기와 복구 정책 실행
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 컴팩트 생성자에서 네 컴포넌트의 처리를 비교하고 변경 스트림 자리의 주석을 읽는다.
|
||||
2. 시작 검증기의 변경 스트림 분기, 그 인자를 넘기는 곳, 검증기를 만드는 곳을 전수로 센다.
|
||||
3. 그 분기가 내는 문장이 저장소 어디에 나오는지 센다.
|
||||
4. 검증기를 만드는 시험의 네 호출이 각각 어디서 멈추는지 읽는다.
|
||||
5. 오플로그가 없는 단독 서버를 띄운다.
|
||||
6. 세 입력을 실제 바인딩에 통과시키고 각각의 결과를 읽는다.
|
||||
7. 같은 서버에 대해 출하 배선과 리터럴 참 배선으로 시작 검증을 각각 돌린다.
|
||||
8. 같은 클래스로 커서를 열고, 그 실패를 복구 정책에 넘긴다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
설정 타입의 주석은 이 값을 저장하지 않고 거부한다고 적으면서, 근거로 드라이버 쪽 구현이 없다는 것을 든다.
|
||||
|
||||
## 네 값 중 하나만 삼켜진다
|
||||
|
||||
:::evidence key="a06-f003-change-streams-true" alt="설정 타입의 컴팩트 생성자에서 네 컴포넌트가 각각 흡수·보존·덮어쓰기·예외로 처리되는 구간과 변경 스트림 자리의 주석, 시작 검증기의 변경 스트림 분기, 그 분기가 내는 문장이 저장소에 나오는 곳 전수, changeStreams 라는 이름이 나오는 곳 전수, 검증기를 만드는 곳 전수를 출력한 터미널 기록." caption="프로파일은 빈 맵으로 흡수 · 음수 보조 노드 수는 예외 · 트랜잭션은 보존 · 변경 스트림만 예외 없이 거짓 · 분기가 내는 문장은 자기 throw 자리 한 곳뿐 · 검증기 생성 지점은 프로덕션 1 시험 1 — 43줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
```java
|
||||
// Experimental, and therefore not a switch (MNG-INT-003). The driver-side source — watch,
|
||||
// resumeAfter/startAfter, cursor lifetime, reconnection — is not shipped; what exists is policy
|
||||
// and value objects that do not add up to a running consumer. Accepting the flag and ignoring
|
||||
// it
|
||||
// would leave an operator believing it took effect, so the value is refused rather than stored:
|
||||
// zero beans, zero threads, and a `true` that cannot be honoured never becomes one that looks
|
||||
// honoured.
|
||||
changeStreams = false;
|
||||
```
|
||||
|
||||
여덟 줄 아래에서 같은 생성자가 음수 보조 노드 수를 예외로 던진다. 거부가 어떤 모양인지 같은 생성자 안에 있고, 이 줄은 그 모양이 아니다. 형제 불리언인 `transactions` 는 손대지 않는다.
|
||||
|
||||
## 검증기가 받는 값은 언제나 거짓이다
|
||||
|
||||
```java
|
||||
if (changeStreamsEnabled && !capabilities.isStable(MongoCapability.CHANGE_STREAM)) {
|
||||
```
|
||||
|
||||
좌항을 넘기는 프로덕션 지점은 자동 구성 한 줄뿐이다. 검증기를 만드는 곳은 그 줄과 시험 하나이고, 시험은 리터럴을 넘긴다. 그런데 그 시험의 네 호출 어느 것도 이 `if` 의 본문을 실행하지 않는다.
|
||||
|
||||
- 단독 서버에 프로덕션 프로파일을 놓는 호출은 참 둘을 넘기지만, `validate()` 가 능력 검사보다 먼저 부르는 토폴로지 검사에서 멈춘다
|
||||
- 선언과 실제가 어긋나는 호출은 거짓 둘을 넘긴다
|
||||
- 트랜잭션만 켜는 호출은 변경 스트림에 거짓을 넘긴다
|
||||
- 정상 복제 셋 호출은 참을 넘기지만 그 토폴로지에서는 능력이 안정으로 나온다
|
||||
|
||||
이 `if` 가 만드는 문장은 저장소 전체에서 자기 `throw` 자리 한 곳에만 있다.
|
||||
|
||||
## 실제 서버에서
|
||||
|
||||
:::evidence key="a06-f003-change-streams-true-probe" alt="오플로그가 없는 단독 MongoDB 를 띄우고 세 입력을 실제 바인딩에 통과시킨 결과, 관측 토폴로지와 변경 스트림 능력 판정, 출하 배선과 리터럴 참 배선으로 각각 돌린 시작 검증 결과, 같은 클래스를 직접 만들어 커서를 연 결과, 그리고 그 실패를 복구 정책에 넘긴 판정을 출력한 터미널 기록." caption="단독 서버에서 능력 판정은 isStable=false · 출하 배선의 시작 검증은 통과, 리터럴 참 배선은 토폴로지를 지목하며 거절 · 커서를 열면 드라이버가 40573 으로 거절 · 복구 정책은 FAILED 와 failover 런북 — 20줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
오플로그가 없는 단독 서버를 띄우고 같은 클래스들을 그대로 썼다. 리터럴 참을 넘긴 배선의 예외 메시지는 이 배포에 없는 것과 필요한 것을 함께 적는다.
|
||||
|
||||
```text
|
||||
change streams are enabled but unavailable here: {reason=topology is STANDALONE, which has no oplog}; a REPLICA_SET, SHARDED or ATLAS topology is required
|
||||
```
|
||||
|
||||
출하 배선에서는 이 예외가 만들어지지 않는다. 같은 클래스를 직접 만들어 커서를 열면 드라이버가 명령 단계에서 거절한다.
|
||||
|
||||
```text
|
||||
Command execution failed on MongoDB server with error 40573 (Location40573): 'The $changeStream stage is only supported on replica sets' on server 127.0.0.1:57017.
|
||||
```
|
||||
|
||||
그 실패를 복구 정책에 넘기면 판정이 나온다.
|
||||
|
||||
```text
|
||||
MongoChangeStreamRecoveryDecision[state=FAILED, autoResume=false, requiredRunbook=docs/mongodb/runbooks/failover.md]
|
||||
```
|
||||
|
||||
정책은 서버 코드가 이력 소실이면 전용 런북을, 라벨이 재개 가능이면 재개를 고르고, 그 밖은 실패로 확정한다. 40573 은 셋째 갈래다. 붙는 런북은 프라이머리 선출과 서버 선택 지연을 다루고, 이 서버에 오플로그가 없다는 경우는 다루지 않는다.
|
||||
|
||||
## 주석이 근거로 든 사실
|
||||
|
||||
:::evidence key="a06-f003-change-streams-true-shipped" alt="드라이버 쪽 구현을 만드는 빈 위에 겹쳐 있는 조건 넷(모듈 opt-in, 블로킹 템플릿 클래스, 리액티브 템플릿 클래스와 빈), 그 빈이 커서를 여는 호출 사슬, 소비자 빈이 요구하는 조건 다섯, 그 다섯을 구현하는 클래스 전수, 저장소 자신의 배선 시험 네 개, 그리고 이 자동 구성 파일에서 해당 플래그가 나오는 유일한 줄을 출력한 터미널 기록." caption="source 빈의 조건은 모듈 opt-in 과 리액티브 템플릿, 그리고 @ConditionalOnMissingBean · 소비자 조건 다섯의 구현체 열은 전부 시험 픽스처 · 배선 시험이 조립·미조립·미배선을 각각 고정 · 플래그는 검증기 인자 한 줄뿐 — 56줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
드라이버 쪽 구현에는 빈 선언이 있다. 그 빈의 메서드에 붙은 조건은 `@ConditionalOnMissingBean` 하나지만, 그것을 감싼 중첩 설정 클래스가 리액티브 템플릿을 클래스로도 빈으로도 요구하고, 다시 그 바깥이 모듈 opt-in 을 요구한다. 무조건은 아니고 리액티브 템플릿이 있는 배포에서는 언제나다. 그 조건 넷 어디에도 변경 스트림 플래그는 없다.
|
||||
|
||||
소비자 빈의 선언도 있다. 조건이 다섯인데 다섯 다 배포가 공급해야 하는 타입이고, 이 저장소는 다섯 중 어느 것도 빈으로 만들지 않는다. 열 개의 구현체가 전부 시험 안의 `private static final class` 다.
|
||||
|
||||
저장소 자신의 배선 시험이 세 상태를 각각 고정한다. 리액티브 템플릿이 없으면 아무것도 만들지 않고, 있으면 source 는 만들고 투영기가 없으면 소비자는 만들지 않고, 다섯을 주면 소비자가 조립된다.
|
||||
|
||||
주석이 없다고 적은 넷 가운데 `watch` 와 `resumeAfter`/`startAfter` 는 이 클래스에 있고, 커서 수명과 재연결은 같은 패키지의 소비자에 있다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
소비자 빈의 조건 다섯을 공급하는 포크의 배포는 다루지 않았다. 이 저장소 안에서 확인한 것은 다섯 타입의 구현체가 전부 시험 픽스처라는 것, 조건 목록에 이 플래그가 없다는 것, 그리고 저장소 자신의 배선 시험이 그 세 상태를 각각 고정한다는 것이다.
|
||||
|
||||
<!-- body:end -->
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-advanced-compat-f01
|
||||
title: 통합 다리의 메타데이터 조립이 메타데이터 예산을 검사하지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-advanced-compat-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-compat-f01
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-compat-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-compat-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-compat.md#L120 이다.
|
||||
module: grpc-advanced-compat
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 통합 다리의 메타데이터 조립이 메타데이터 예산을 검사하지 않는다
|
||||
|
||||
허용 목록으로 키를 거르지만 값의 크기도, 합계도 보지 않는다. 클래스 javadoc 자신이 예산을 이 정책의 이유 중 하나로 든다 — 흐름의 내부 배관을 네트워크로 보내면 "it counts against the metadata budget." 그리고 같은 저장소의 다른 메타데이터 경로는 예산을 검사한다.
|
||||
|
||||
## 문제
|
||||
|
||||
허용 목록으로 키를 거르지만 값의 크기도, 합계도 보지 않는다.
|
||||
|
||||
클래스 javadoc 자신이 예산을 이 정책의 이유 중 하나로 든다 — 흐름의 내부 배관을 네트워크로 보내면 "it counts against the metadata budget." 그리고 같은 저장소의 다른 메타데이터 경로는 예산을 검사한다.
|
||||
|
||||
## 결론
|
||||
|
||||
String.valueOf(value) 이므로 헤더 값이 임의의 객체일 때 그 문자열 표현이 그대로 실린다.
|
||||
|
||||
Spring Integration 헤더에는 컬렉션이나 도메인 객체가 흔히 들어가므로 값 하나가 클 수 있다.
|
||||
|
||||
수정은 이 record 에 GrpcMetadataBudget 를 성분으로 추가하고 metadataFrom 끝에서 검사하는 것이다.
|
||||
|
||||
형태가 이미 옆 리프에 있다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcMetadataBudget 참조 26건 전수 검색과 다리의 메타데이터 조립 경로 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-advanced-compat.md#L120 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
허용 목록으로 키를 거르지만 값의 크기도, 합계도 보지 않는다.
|
||||
|
||||
## GrpcMetadataBudget 참조 위치
|
||||
|
||||
:::evidence key="grpc-advanced-compat-f01" alt="코드베이스에서 GrpcMetadataBudget 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcMetadataBudget 코드베이스 검색 — 26줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 클래스 javadoc 자신이 예산을 이유로 든다
|
||||
|
||||
흐름의 내부 배관을 네트워크로 보내면 "it counts against the metadata budget." 그리고 같은 저장소의 다른 메타데이터 경로는 예산을 검사한다.
|
||||
|
||||
## 값 하나가 클 수 있다
|
||||
|
||||
`String.valueOf(value)` 이므로 헤더 값이 임의의 객체일 때 그 문자열 표현이 그대로 실린다. Spring Integration 헤더에는 컬렉션이나 도메인 객체가 흔히 들어간다.
|
||||
|
||||
## 수정
|
||||
|
||||
이 record 에 `GrpcMetadataBudget` 를 성분으로 추가하고 `metadataFrom` 끝에서 검사하는 것이다. 형태가 이미 옆 리프에 있다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제 브라우저·프록시·서블릿 컨테이너로 이 다리를 돌리지 않았다. 그 인프라가 없다는 것이 이 가족의 기록이다.
|
||||
|
||||
<!-- body:end -->
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-core-api-f01
|
||||
title: 정책 목록의 가장 강한 성질을 이 저장소에서는 쓸 수 없다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-core-api-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-core-api-f01
|
||||
file: ../../../final/evidence/rendered/grpc-core-api-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-core-api-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-core-api.md#L140 이다.
|
||||
module: grpc-core-api
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 정책 목록의 가장 강한 성질을 이 저장소에서는 쓸 수 없다
|
||||
|
||||
withDescriptorMethods 를 부르는 곳은 이 리프의 테스트 두 줄뿐이다. GrpcMethodPolicyCatalog.builder() 를 부르는 곳은 저장소 전체에서 전부 테스트다.
|
||||
|
||||
## 문제
|
||||
|
||||
withDescriptorMethods 를 부르는 곳은 이 리프의 테스트 두 줄뿐이다.
|
||||
|
||||
GrpcMethodPolicyCatalog.builder() 를 부르는 곳은 저장소 전체에서 전부 테스트다.
|
||||
|
||||
## 결론
|
||||
|
||||
그리고 그중 어느 것도 서술자 집합을 선언하지 않는다(위 두 줄 제외).
|
||||
|
||||
자바독이 그 상태를 미리 서술한다 — 서술자가 없으면 "the catalog is materially weaker … there is nothing to compare a policy's method name against." 그리고 서술자가 없는 이유는 옆 리프에 있다.
|
||||
|
||||
grpc-codegen 이 서술자 산출물을 정의하지만 저장소에 protobuf 플러그인이 없어 protoc 이 돌지 않는다.
|
||||
|
||||
즉 이름 변경을 잡는 성질은 코드 생성 레인이 켜지기 전까지 성립할 수 없다.
|
||||
|
||||
기록하는 이유는 이것이 이 클래스가 존재하는 첫 번째 이유로 적혀 있기 때문이다.
|
||||
|
||||
수정은 코드 생성 레인이 생길 때 그 서술자를 목록 조립에 연결하는 것이고, 그때까지는 자바독이 그 조건을 명시하는 편이 낫다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : withDescriptorMethods 와 builder() 호출처 전수 검색으로 production 호출 유무 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-core-api.md#L140 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`withDescriptorMethods` 를 부르는 곳은 이 리프의 테스트 두 줄뿐이다.
|
||||
|
||||
```
|
||||
grpc-core-api/src/test/.../GrpcMethodPolicyCatalogTest.java:145
|
||||
grpc-core-api/src/test/.../GrpcMethodPolicyCatalogTest.java:175
|
||||
```
|
||||
|
||||
`GrpcMethodPolicyCatalog.builder()` 를 부르는 곳은 저장소 전체에서 전부 테스트이고, 그중 어느 것도 서술자 집합을 선언하지 않는다(위 두 줄 제외).
|
||||
|
||||
## withDescriptorMethods 를 부르는 곳
|
||||
|
||||
:::evidence key="grpc-core-api-f01" alt="분석 문서 analysis/grpc/grpc-core-api.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-core-api.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 자바독이 그 상태를 미리 서술한다
|
||||
|
||||
서술자가 없으면 "the catalog is materially weaker … there is nothing to compare a policy's method name against."
|
||||
|
||||
## 서술자가 없는 이유는 옆 리프에 있다
|
||||
|
||||
`grpc-codegen` 이 서술자 산출물을 정의하지만 저장소에 protobuf 플러그인이 없어 `protoc` 이 돌지 않는다. 즉 이름 변경을 잡는 성질은 코드 생성 레인이 켜지기 전까지 성립할 수 없다.
|
||||
|
||||
## 기록하는 이유
|
||||
|
||||
이것이 이 클래스가 존재하는 첫 번째 이유로 적혀 있다. 수정은 코드 생성 레인이 생길 때 그 서술자를 목록 조립에 연결하는 것이고, 그때까지는 자바독이 그 조건을 명시하는 편이 낫다. P3.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
서술자 대조 경로를 실제 스키마로 돌려 보지 않았다. 저장소에 컴파일된 서술자가 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-observability-f01
|
||||
title: queueHighWatermark 는 요구되고 검증되지만 아무도 읽지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-observability-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-observability-f01
|
||||
file: ../../../final/evidence/rendered/grpc-observability-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-observability-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-observability.md#L197 이다.
|
||||
module: grpc-observability
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# queueHighWatermark 는 요구되고 검증되지만 아무도 읽지 않는다
|
||||
|
||||
GrpcStreamObservation 의 7성분 중 queueHighWatermark 만 소비자가 없다. tags() 에 없고, GrpcObservationConvention.record(GrpcStreamObservation) 이 등록하는 세 meter(STREAM_LIFETIME·STREAM_MESSAGES·STREAM_FLOW_CONTROL_STALLS) 어디에도 들어가지 않는다.
|
||||
|
||||
## 문제
|
||||
|
||||
GrpcStreamObservation 의 7성분 중 queueHighWatermark 만 소비자가 없다.
|
||||
|
||||
tags() 에 없고, GrpcObservationConvention.record(GrpcStreamObservation) 이 등록하는 세 meter(STREAM_LIFETIME·STREAM_MESSAGES·STREAM_FLOW_CONTROL_STALLS) 어디에도 들어가지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
테스트도 250L 을 넘기고 그 값에 대해 아무것도 단언하지 않는다.
|
||||
|
||||
클래스 javadoc 이 "what is recorded instead is …" 로 세 가지를 열거하는데 그 목록에도 없다.
|
||||
|
||||
즉 서술과 구현은 일치하고, 어긋난 것은 필수 생성자 인자라는 점이다.
|
||||
|
||||
호출자는 측정해서 넘겨야 하고 그 값은 버려진다.
|
||||
|
||||
수정은 둘 중 하나다 — STREAM_QUEUE_HIGH_WATERMARK gauge/counter 를 추가하거나, 성분에서 뺀다.
|
||||
|
||||
큐 최고 수위는 소비자 지연의 직접 지표이므로 전자가 이 클래스의 목적에 맞는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcStreamObservation 참조 5건 검색과 record 오버로드가 등록하는 meter 목록 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-observability.md#L197 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GrpcStreamObservation` 의 7성분 중 `queueHighWatermark` 만 소비자가 없다.
|
||||
|
||||
```
|
||||
GrpcStreamObservation.java:23 long queueHighWatermark, ← 선언
|
||||
GrpcStreamObservation.java:31 … || queueHighWatermark < 0 ← 검증
|
||||
그 외 저장소 전체 매치 0
|
||||
```
|
||||
|
||||
## GrpcStreamObservation 참조 위치
|
||||
|
||||
:::evidence key="grpc-observability-f01" alt="코드베이스에서 GrpcStreamObservation 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcStreamObservation 코드베이스 검색 — 5줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 세 meter 어디에도 들어가지 않는다
|
||||
|
||||
`tags()` 에 없고, `GrpcObservationConvention.record(GrpcStreamObservation)` 이 등록하는 `STREAM_LIFETIME`·`STREAM_MESSAGES`·`STREAM_FLOW_CONTROL_STALLS` 어디에도 없다. 테스트도 `250L` 을 넘기고 그 값에 대해 아무것도 단언하지 않는다.
|
||||
|
||||
## 서술과 구현은 일치한다
|
||||
|
||||
클래스 javadoc 이 "what is recorded instead is …" 로 세 가지를 열거하는데 그 목록에도 없다. 어긋난 것은 **필수 생성자 인자**라는 점이다 — 호출자는 측정해서 넘겨야 하고 그 값은 버려진다.
|
||||
|
||||
## 수정
|
||||
|
||||
`STREAM_QUEUE_HIGH_WATERMARK` gauge/counter 를 추가하거나, 성분에서 뺀다. 큐 최고 수위는 소비자 지연의 직접 지표이므로 전자가 이 클래스의 목적에 맞는다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
이 리프를 실제 MeterRegistry 에 배선해 돌린 적이 없다. 배선 자체가 없으므로 런타임 관측이 불가능하다.
|
||||
|
||||
<!-- body:end -->
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-policy-f07
|
||||
title: clearAfterTask 는 합법 값이 하나뿐인 성분이고, 아무도 읽지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-policy-f07
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-policy-f07
|
||||
file: ../../../final/evidence/rendered/grpc-policy-f07.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-policy-f07.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-policy.md#L324 이다.
|
||||
module: grpc-policy
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# clearAfterTask 는 합법 값이 하나뿐인 성분이고, 아무도 읽지 않는다
|
||||
|
||||
false 를 무조건 거부하므로 이 성분이 가질 수 있는 값은 true 하나다. 그리고 저장소 전체에서 clearAfterTask() 를 읽는 production 코드가 없다 — 호출처는 이 생성자의 가드와 테스트의 단언 한 줄뿐이다.
|
||||
|
||||
## 문제
|
||||
|
||||
false 를 무조건 거부하므로 이 성분이 가질 수 있는 값은 true 하나다.
|
||||
|
||||
그리고 저장소 전체에서 clearAfterTask() 를 읽는 production 코드가 없다 — 호출처는 이 생성자의 가드와 테스트의 단언 한 줄뿐이다.
|
||||
|
||||
## 결론
|
||||
|
||||
읽지 않아도 되는 이유는 GrpcContextBinder 가 옳게 쓰였기 때문이다.
|
||||
|
||||
runWith·callWith·wrap 이 전부 finally 에서 detach 한다.
|
||||
|
||||
불변식이 이미 구조로 지켜진다.
|
||||
|
||||
그래서 이 성분은 설정처럼 보이지만 설정이 아니다.
|
||||
|
||||
읽는 사람은 정책으로 끌 수 있는 것이라고 읽고, 테스트는 그 가드를 시험한다.
|
||||
|
||||
수정은 성분을 지우고 javadoc 에 "always cleared" 를 남기는 것이다.
|
||||
|
||||
그러면 backgroundWork()·stable() 이 인자 하나가 되고, 불변식은 검증이 아니라 구조가 된다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcContextBinder 참조 11건 검색과 생성자 가드가 허용하는 값 범위 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-policy.md#L324 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`false` 를 무조건 거부하므로 이 성분이 가질 수 있는 값은 `true` 하나다. 그리고 저장소 전체에서 `clearAfterTask()` 를 읽는 production 코드가 없다 — 호출처는 이 생성자의 가드와 테스트의 단언 한 줄뿐이다.
|
||||
|
||||
## GrpcContextBinder 참조 위치
|
||||
|
||||
:::evidence key="grpc-policy-f07" alt="코드베이스에서 GrpcContextBinder 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcContextBinder 코드베이스 검색 — 11줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 읽지 않아도 되는 이유
|
||||
|
||||
`GrpcContextBinder` 가 옳게 쓰였다 — `runWith`·`callWith`·`wrap` 이 전부 `finally` 에서 detach 한다. 불변식이 이미 구조로 지켜진다.
|
||||
|
||||
## 그래서 설정처럼 보이지만 설정이 아니다
|
||||
|
||||
읽는 사람은 정책으로 끌 수 있는 것이라고 읽고, 테스트는 그 가드를 시험한다. 수정은 성분을 지우고 javadoc 에 "always cleared" 를 남기는 것이다. 그러면 `backgroundWork()`·`stable()` 이 인자 하나가 되고, 불변식은 검증이 아니라 구조가 된다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
이 성분을 false 로 만들어 동작 차이를 관측하지 않았다. 생성자가 false 를 무조건 거부한다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-server-f03
|
||||
title: 빌더 경로에서 순서 규칙 넷 중 셋이 발화할 수 없다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-server-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-server-f03
|
||||
file: ../../../final/evidence/rendered/grpc-server-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-server-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-server.md#L196 이다.
|
||||
module: grpc-server
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 빌더 경로에서 순서 규칙 넷 중 셋이 발화할 수 없다
|
||||
|
||||
byStage 는 EnumMap 이므로 keySet() 은 언제나 열거형 선언 순서다. 그리고 stage(...) 가 같은 단계의 두 번째 등록을 이미 거부한다.
|
||||
|
||||
## 문제
|
||||
|
||||
byStage 는 EnumMap 이므로 keySet() 은 언제나 열거형 선언 순서다.
|
||||
|
||||
그리고 stage(...) 가 같은 단계의 두 번째 등록을 이미 거부한다.
|
||||
|
||||
## 결론
|
||||
|
||||
따라서 빌더가 만드는 목록에서는 중복도, 역순도, 예외 경계가 최외곽이 아닌 경우도 발생할 수 없다.
|
||||
|
||||
발화 가능한 규칙은 필수 단계 누락 하나다.
|
||||
|
||||
결함은 아니다 — 나머지 셋은 violations(List) 를 직접 부르는 외부 호출자를 위한 것이고, 테스트가 그 경로로 셋을 모두 확인한다.
|
||||
|
||||
기록하는 이유는 빌더를 쓰는 조립 코드가 그 셋의 보호를 받는다고 읽기 쉽기 때문이다.
|
||||
|
||||
실제 보호는 자료구조가 준다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : EnumMap 의 keySet 순서 보장과 stage 등록 가드가 이미 막는 경우의 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-server.md#L196 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`byStage` 는 `EnumMap` 이므로 `keySet()` 은 언제나 열거형 선언 순서다. 그리고 `stage(...)` 가 같은 단계의 두 번째 등록을 이미 거부한다.
|
||||
|
||||
## byStage 가 EnumMap 이다
|
||||
|
||||
:::evidence key="grpc-server-f03" alt="분석 문서 analysis/grpc/grpc-server.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-server.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 빌더 경로에서는 셋이 발생할 수 없다
|
||||
|
||||
중복도, 역순도, 예외 경계가 최외곽이 아닌 경우도 발생할 수 없다. 발화 가능한 규칙은 필수 단계 누락 하나다.
|
||||
|
||||
## 결함은 아니다
|
||||
|
||||
나머지 셋은 `violations(List)` 를 직접 부르는 외부 호출자를 위한 것이고, 테스트가 그 경로로 셋을 모두 확인한다. 기록하는 이유는 빌더를 쓰는 조립 코드가 그 셋의 보호를 받는다고 읽기 쉽기 때문이다 — 실제 보호는 자료구조가 준다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
빌더 경로로 네 규칙을 실제로 발화시켜 보지 않았다. 자료구조의 순서 보장과 선행 가드로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-spring-boot-starter-f01
|
||||
title: 시작 검증기가 시작 시 실행되지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-spring-boot-starter-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-spring-boot-starter-f01
|
||||
file: ../../../final/evidence/rendered/grpc-spring-boot-starter-f01.svg
|
||||
- key: grpc-spring-boot-starter-f01-diagram
|
||||
file: ../../../final/assets/diagrams/grpc-spring-boot-starter-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-spring-boot-starter-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-spring-boot-starter.md#L183 이다.
|
||||
module: grpc-spring-boot-starter
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 시작 검증기가 시작 시 실행되지 않는다
|
||||
|
||||
GrpcPlatformStartupValidator 를 이름으로 부르는 파일은 둘뿐이다 — 자기 자신과 자기 테스트. GrpcPlatformAutoConfiguration 은 빈 9개를 만들고 requireValid 를 부르지 않는다.
|
||||
|
||||
## 문제
|
||||
|
||||
GrpcPlatformStartupValidator 를 이름으로 부르는 파일은 둘뿐이다 — 자기 자신과 자기 테스트.
|
||||
|
||||
GrpcPlatformAutoConfiguration 은 빈 9개를 만들고 requireValid 를 부르지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
초기화 콜백도, @PostConstruct 도, ApplicationRunner 도 없다.
|
||||
|
||||
그래서 클래스 javadoc 이 약속한 성질이 성립하지 않는다 — "Refuses to start on a configuration that would be wrong in a way nobody would notice." 지금은 그 설정으로 그냥 시작한다.
|
||||
|
||||
검증기가 유일한 소비자인 설정 키가 넷이다.
|
||||
|
||||
transport — production 이 아닌 전송을 거부할 곳이 없다.
|
||||
|
||||
게다가 자동 설정은 이 값을 보지 않고 GrpcServerProfile.stableNetty(...) 를 하드코딩한다(§17.2).
|
||||
|
||||
tls-enabled · trust-all-certificates — 배포 환경의 TLS 바닥을 강제할 곳이 없다.
|
||||
|
||||
operation-ledger-enabled — 멱등 키 필수 메서드가 원장 없이 열리는 것을 막을 곳이 없다.
|
||||
|
||||
같은 저장소가 이 형태를 두 번 기록했다 — WebPlatformStartupValidator 가 시작 시 실행되지 않고, BrokerAclManifest 의 시작 자기점검이 없다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcPlatformStartupValidator 참조 15건 검색과 자동 설정이 만드는 빈 목록 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-spring-boot-starter.md#L183 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GrpcPlatformStartupValidator` 를 이름으로 부르는 파일은 둘뿐이다 — 자기 자신과 자기 테스트.
|
||||
|
||||
## 검증기가 도는 데 빠진 것
|
||||
|
||||
:::evidence key="grpc-spring-boot-starter-f01-diagram" alt="설정 프로퍼티 셋만 검증기 입력 안에 놓이고 모듈 id 집합 생산자와 requireValid 호출 지점이 바깥에 빗금으로 놓인다" caption="검증기가 도는 데 빠진 것" zoom="false"
|
||||
:::
|
||||
|
||||
`GrpcPlatformAutoConfiguration` 은 빈 9개를 만들고 `requireValid` 를 부르지 않는다. 초기화 콜백도, `@PostConstruct` 도, `ApplicationRunner` 도 없다. 그래서 클래스 javadoc 이 약속한 성질이 성립하지 않는다 — "Refuses to start on a configuration that would be wrong in a way nobody would notice."
|
||||
|
||||
## GrpcPlatformStartupValidator 참조 위치
|
||||
|
||||
:::evidence key="grpc-spring-boot-starter-f01" alt="코드베이스에서 GrpcPlatformStartupValidator 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcPlatformStartupValidator 코드베이스 검색 — 15줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 함께 사라지는 설정 키 넷
|
||||
|
||||
`transport` — production 이 아닌 전송을 거부할 곳이 없다(게다가 자동 설정은 이 값을 보지 않고 `GrpcServerProfile.stableNetty(...)` 를 하드코딩한다, §17.2). `tls-enabled` · `trust-all-certificates` — 배포 환경의 TLS 바닥을 강제할 곳이 없다. `operation-ledger-enabled` — 멱등 키 필수 메서드가 원장 없이 열리는 것을 막을 곳이 없다.
|
||||
|
||||
## 정본이 저장소 안에 둘 있다
|
||||
|
||||
messaging 의 `StartupProfileValidation` 은 `InitializingBean.afterPropertiesSet` 으로 돌려 그 문제를 이미 한 번 해결했고, fileserver 는 `attestMapping()` 을 app-bootstrap 의 `@Bean` 으로 연결했다. 반대로 같은 저장소가 이 형태를 두 번 기록했다 — `WebPlatformStartupValidator` 가 시작 시 실행되지 않고, `BrokerAclManifest` 의 시작 자기점검이 없다.
|
||||
|
||||
## 왜 배선되지 않았는지가 서명에 보인다
|
||||
|
||||
`violations` 는 넷을 받고 그중 셋에 생산자가 없다. 특히 마지막은 "스타터가 해석한 모듈 id 집합" 인데 그것을 실행 중에 산출하는 코드가 없다. P2.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
스타터를 실제 애플리케이션에 올려 컨텍스트를 세우지 않았다. build-only 이고 이 스타터를 의존하는 모듈이 저장소에 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-spring-boot-starter-f03
|
||||
title: default-unary-deadline 은 읽는 코드가 저장소에 없다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-spring-boot-starter-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-spring-boot-starter-f03
|
||||
file: ../../../final/evidence/rendered/grpc-spring-boot-starter-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-spring-boot-starter-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-spring-boot-starter.md#L236 이다.
|
||||
module: grpc-spring-boot-starter
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# default-unary-deadline 은 읽는 코드가 저장소에 없다
|
||||
|
||||
getDefaultUnaryDeadline() 의 호출자가 0 이다. 검증기도 이 값을 쓰지 않는다 — 검증기가 보는 것은 정책 목록의 policy.deadline().usable() 이고 그 값이 0 이면 위반을 낸다.
|
||||
|
||||
## 문제
|
||||
|
||||
getDefaultUnaryDeadline() 의 호출자가 0 이다.
|
||||
|
||||
검증기도 이 값을 쓰지 않는다 — 검증기가 보는 것은 정책 목록의 policy.deadline().usable() 이고 그 값이 0 이면 위반을 낸다.
|
||||
|
||||
## 결론
|
||||
|
||||
즉 자바독이 말하는 "선언하지 않은 메서드에 적용되는 기본 마감" 을 적용하는 코드가 없다.
|
||||
|
||||
ignoreUnknownFields = false 라서 이 키를 설정하는 것은 성공하고 아무 효과가 없다.
|
||||
|
||||
수정은 그 기본값을 실제로 적용하는 지점을 만들거나(정책 목록 조립 시), 필드를 제거하는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : getDefaultUnaryDeadline 호출처 검색과 검증기가 실제로 읽는 값 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-spring-boot-starter.md#L236 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`getDefaultUnaryDeadline()` 의 호출자가 0 이다. 검증기도 이 값을 쓰지 않는다 — 검증기가 보는 것은 정책 목록의 `policy.deadline().usable()` 이고 그 값이 0 이면 위반을 낸다.
|
||||
|
||||
## 검증기가 실제로 보는 값
|
||||
|
||||
:::evidence key="grpc-spring-boot-starter-f03" alt="분석 문서 analysis/grpc/grpc-spring-boot-starter.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-spring-boot-starter.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 자바독이 말하는 기본 마감을 적용하는 코드가 없다
|
||||
|
||||
"선언하지 않은 메서드에 적용되는 기본 마감" 이다. `ignoreUnknownFields = false` 라서 이 키를 설정하는 것은 성공하고 아무 효과가 없다.
|
||||
|
||||
## 수정
|
||||
|
||||
그 기본값을 실제로 적용하는 지점을 만들거나(정책 목록 조립 시), 필드를 제거하는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
이 값을 바꿔 동작 차이가 없음을 실행으로 확인하지 않았다. 호출자가 0 이라는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-testkit-f05
|
||||
title: 계약 스위트 둘이 결과를 만드는 코드를 갖지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-testkit-f05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-testkit-f05
|
||||
file: ../../../final/evidence/rendered/grpc-testkit-f05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-testkit-f05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-testkit.md#L245 이다.
|
||||
module: grpc-testkit
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 계약 스위트 둘이 결과를 만드는 코드를 갖지 않는다
|
||||
|
||||
GrpcUnaryReliabilityContract 와 GrpcServerStreamingContract 는 순수 평가기다 — List<Result> 를 받아 위반을 돌려준다. 시나리오 정의(단항 3 · 스트리밍 5)와 그 정합성 검사는 훌륭하다.
|
||||
|
||||
## 문제
|
||||
|
||||
GrpcUnaryReliabilityContract 와 GrpcServerStreamingContract 는 순수 평가기다 — List<Result> 를 받아 위반을 돌려준다.
|
||||
|
||||
시나리오 정의(단항 3 · 스트리밍 5)와 그 정합성 검사는 훌륭하다.
|
||||
|
||||
## 결론
|
||||
|
||||
스위트가 자기 커버리지를 열거하고, 돌지 않은 시나리오를 침묵이 아니라 위반으로 만든다.
|
||||
|
||||
빠진 것은 그 시나리오를 돌리는 쪽이다.
|
||||
|
||||
GrpcUnaryContractResult·GrpcStreamingContractResult 를 만드는 코드는 저장소 전체에서 두 테스트뿐이고, 둘 다 리터럴로 만든다.
|
||||
|
||||
그래서 "이 플랫폼은 비멱등 변경을 재시도하지 않는다" 를 뒷받침하는 것은, 그 문장을 리터럴로 적은 뒤 평가기가 그것을 읽고 위반이 없다고 답하는 절차다.
|
||||
|
||||
평가기의 산술은 옳고, 대상이 관측이 아니다.
|
||||
|
||||
in-process 픽스처(§1)는 이 시나리오들을 돌릴 재료를 이미 갖고 있다 — 인터셉터를 끼운 서버, 상태 매핑, 스트리밍 핸들러.
|
||||
|
||||
수정은 픽스처 위에서 세 시나리오를 실행해 attempts·businessInvocations 를 세는 러너를 두는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcUnaryReliabilityContract 참조 10건 검색과 결과를 만드는 코드의 존재 여부 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-testkit.md#L245 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GrpcUnaryReliabilityContract` 와 `GrpcServerStreamingContract` 는 순수 평가기다 — `List<Result>` 를 받아 위반을 돌려준다. 시나리오 정의(단항 3 · 스트리밍 5)와 그 정합성 검사는 훌륭하다. 스위트가 자기 커버리지를 열거하고, 돌지 않은 시나리오를 침묵이 아니라 위반으로 만든다.
|
||||
|
||||
## GrpcUnaryReliabilityContract 참조 위치
|
||||
|
||||
:::evidence key="grpc-testkit-f05" alt="코드베이스에서 GrpcUnaryReliabilityContract 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcUnaryReliabilityContract 코드베이스 검색 — 10줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 빠진 것은 그 시나리오를 돌리는 쪽이다
|
||||
|
||||
`GrpcUnaryContractResult`·`GrpcStreamingContractResult` 를 만드는 코드는 저장소 전체에서 두 테스트뿐이고, 둘 다 리터럴로 만든다. 그래서 "이 플랫폼은 비멱등 변경을 재시도하지 않는다" 를 뒷받침하는 것은, 그 문장을 리터럴로 적은 뒤 평가기가 그것을 읽고 위반이 없다고 답하는 절차다.
|
||||
|
||||
## 평가기의 산술은 옳고 대상이 관측이 아니다
|
||||
|
||||
in-process 픽스처(§1)는 이 시나리오들을 돌릴 재료를 이미 갖고 있다 — 인터셉터를 끼운 서버, 상태 매핑, 스트리밍 핸들러. 수정은 픽스처 위에서 세 시나리오를 실행해 `attempts`·`businessInvocations` 를 세는 러너를 두는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
두 계약 스위트를 실제 결과로 돌려 보지 않았다. 순수 평가기이고 입력을 만드는 코드가 없다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-admin-api-f03
|
||||
title: TopologyManagementMode 가 어디에도 연결되어 있지 않다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-admin-api-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-api-f03
|
||||
file: ../../../final/evidence/rendered/messaging-admin-api-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-api-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-api.md#L952 이다.
|
||||
module: messaging-admin-api
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# TopologyManagementMode 가 어디에도 연결되어 있지 않다
|
||||
|
||||
자기 선언과 테스트 4건이 전부다. 이 enum 을 읽는 프로덕션 코드도, 이것으로 매핑되는 설정 프로퍼티도 없다(EVD-302).
|
||||
|
||||
## 문제
|
||||
|
||||
자기 선언과 테스트 4건이 전부다.
|
||||
|
||||
이 enum 을 읽는 프로덕션 코드도, 이것으로 매핑되는 설정 프로퍼티도 없다(EVD-302).
|
||||
|
||||
## 결론
|
||||
|
||||
두 선택지가 있다: 실제로 배선하거나(선언된 토폴로지 관리 모드를 설정에서 읽고 requireSafeFor(isProduction) 를 기동 시 호출), 제거한다.
|
||||
|
||||
지금 상태는 "규칙이 코드에 있다" 는 인상만 준다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : TopologyManagementMode 참조 5건 검색으로 프로덕션 읽기와 설정 프로퍼티 매핑 유무 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-admin-api.md#L952 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
자기 선언과 테스트 4건이 전부다. 이 enum 을 읽는 프로덕션 코드도, 이것으로 매핑되는 설정 프로퍼티도 없다(`EVD-302`).
|
||||
|
||||
## TopologyManagementMode 참조 위치
|
||||
|
||||
:::evidence key="messaging-admin-api-f03" alt="코드베이스에서 TopologyManagementMode 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TopologyManagementMode 코드베이스 검색 — 5줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 두 선택지
|
||||
|
||||
실제로 배선하거나(선언된 토폴로지 관리 모드를 설정에서 읽고 `requireSafeFor(isProduction)` 를 기동 시 호출), 제거한다. 지금 상태는 "규칙이 코드에 있다" 는 인상만 준다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
부팅된 컨텍스트에서 이 enum 이 어떤 경로로도 읽히지 않는 것을 런타임으로 확인하지 않았다. 참조 전수로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-admin-api-f04
|
||||
title: 운영자용 표면 전체에 프로덕션 소비자가 없다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-admin-api-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-api-f04
|
||||
file: ../../../final/evidence/rendered/messaging-admin-api-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-api-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-api.md#L956 이다.
|
||||
module: messaging-admin-api
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 운영자용 표면 전체에 프로덕션 소비자가 없다
|
||||
|
||||
ReplayPlan.describeImpact, RedrivePlan.describeImpact, ReplayResult.fellShortOfTheEstimate, RedriveResult.isFullyAccounted, AdminOperationLease.isResumption — 다섯 개가 전부 테스트에서만 호출된다(EVD-302). 이것들은 잉여 코드가 아니라 아직 소비자가 없는 잘 설계된 표면이다.
|
||||
|
||||
## 문제
|
||||
|
||||
ReplayPlan.describeImpact, RedrivePlan.describeImpact, ReplayResult.fellShortOfTheEstimate, RedriveResult.isFullyAccounted, AdminOperationLease.isResumption — 다섯 개가 전부 테스트에서만 호출된다(EVD-302).
|
||||
|
||||
이것들은 잉여 코드가 아니라 아직 소비자가 없는 잘 설계된 표면이다.
|
||||
|
||||
## 결론
|
||||
|
||||
describeImpact 의 javadoc 이 "operator-facing" 이라고 쓰고 ApprovedPlanExecutionTest.aReplayIntoTheLiveGroupSaysSoInCapitals 가 대문자 LIVE 까지 검증한다.
|
||||
|
||||
문제는 그 문자열이 도달할 화면이 없다는 것이다.
|
||||
|
||||
admin API·CLI 계층을 만들 때 이 다섯이 그 계층의 명세라는 점을 문서에 남겨 두는 것이 낫다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : ReplayPlan 참조 9건 검색으로 다섯 표면의 호출처가 테스트뿐임을 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-admin-api.md#L956 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`ReplayPlan.describeImpact`, `RedrivePlan.describeImpact`, `ReplayResult.fellShortOfTheEstimate`, `RedriveResult.isFullyAccounted`, `AdminOperationLease.isResumption` — 다섯 개가 전부 테스트에서만 호출된다(`EVD-302`).
|
||||
|
||||
## ReplayPlan 참조 위치
|
||||
|
||||
:::evidence key="messaging-admin-api-f04" alt="코드베이스에서 ReplayPlan 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ReplayPlan 코드베이스 검색 — 9줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 잉여 코드가 아니라 아직 소비자가 없는 표면이다
|
||||
|
||||
`describeImpact` 의 javadoc 이 "operator-facing" 이라고 쓰고 `ApprovedPlanExecutionTest.aReplayIntoTheLiveGroupSaysSoInCapitals` 가 대문자 `LIVE` 까지 검증한다. 문제는 그 문자열이 도달할 화면이 없다는 것이다.
|
||||
|
||||
## 남겨 둘 것
|
||||
|
||||
admin API·CLI 계층을 만들 때 이 다섯이 그 계층의 명세라는 점을 문서에 남겨 두는 것이 낫다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
운영자 도구가 이 저장소 밖에 존재하는지 확인할 수 없었다. 저장소 안의 참조로만 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-admin-api-f06
|
||||
title: messaging-policy 의존이 import 0건이다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-admin-api-f06
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-api-f06
|
||||
file: ../../../final/evidence/rendered/messaging-admin-api-f06.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-api-f06.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-api.md#L968 이다.
|
||||
module: messaging-admin-api
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# messaging-policy 의존이 import 0건이다
|
||||
|
||||
선언만 남아 있다. 제거 후보.
|
||||
|
||||
## 문제
|
||||
|
||||
선언만 남아 있다.
|
||||
|
||||
제거 후보.
|
||||
|
||||
## 결론
|
||||
|
||||
선언만 남아 있다.
|
||||
|
||||
제거 후보.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 선언된 의존에 대한 패키지 이름 import 전수 검색
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-admin-api.md#L968 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`messaging-policy` 의존이 선언만 남아 있고 import 는 0건이다.
|
||||
|
||||
## 선언만 남은 의존
|
||||
|
||||
:::evidence key="messaging-admin-api-f06" alt="분석 문서 analysis/messaging/messaging-admin-api.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-admin-api.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 제거 후보
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
의존을 제거하고 빌드를 돌려 보지 않았다. import 0 건으로 판정했으므로 간접 사용은 배제하지 못했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-admin-runtime-f03
|
||||
title: 오케스트레이터가 어디에서도 실행되지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-admin-runtime-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-runtime-f03
|
||||
file: ../../../final/evidence/rendered/messaging-admin-runtime-f03.svg
|
||||
- key: messaging-admin-runtime-f03-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-admin-runtime-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-runtime-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-runtime.md#L918 이다.
|
||||
module: messaging-admin-runtime
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 오케스트레이터가 어디에서도 실행되지 않는다
|
||||
|
||||
DefaultMessagingAdminService 257줄과 ReplayService 99줄이 프로덕션에서도 테스트에서도 인스턴스화되지 않는다(EVD-307). 검사 순서·저널 시퀀스·실패 시 재던짐·실패 코드 정제가 전부 미검증이다.
|
||||
|
||||
## 문제
|
||||
|
||||
DefaultMessagingAdminService 257줄과 ReplayService 99줄이 프로덕션에서도 테스트에서도 인스턴스화되지 않는다(EVD-307).
|
||||
|
||||
검사 순서·저널 시퀀스·실패 시 재던짐·실패 코드 정제가 전부 미검증이다.
|
||||
|
||||
## 결론
|
||||
|
||||
DefaultMessagingAdminService 의 생성자는 10개 인자를 받고 그중 8개가 SPI 또는 Supplier 이므로, 대역으로 조립하는 테스트를 쓰는 비용은 낮다.
|
||||
|
||||
§12.1(a)의 회귀 테스트도 이 층에서 쓰는 것이 자연스럽다 — 저널·리스·리드라이브 루프가 함께 도는 것이 결함이 나타나는 조건이기 때문이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : DefaultMessagingAdminService 참조 2건 검색으로 프로덕션·테스트 양쪽 인스턴스화 지점 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-admin-runtime.md#L918 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`DefaultMessagingAdminService` 257줄과 `ReplayService` 99줄이 프로덕션에서도 테스트에서도 인스턴스화되지 않는다(`EVD-307`).
|
||||
|
||||
## 실행되지 않는 계층
|
||||
|
||||
:::evidence key="messaging-admin-runtime-f03-diagram" alt="SPI 각각의 단위 테스트만 검증된 범위 안에 놓이고 검사 순서와 저널 시퀀스, 실패 재던짐과 코드 정제가 바깥에 빗금으로 놓인다" caption="실행되지 않는 계층" zoom="false"
|
||||
:::
|
||||
|
||||
검사 순서·저널 시퀀스·실패 시 재던짐·실패 코드 정제가 전부 미검증이다.
|
||||
|
||||
## DefaultMessagingAdminService 참조 위치
|
||||
|
||||
:::evidence key="messaging-admin-runtime-f03" alt="코드베이스에서 DefaultMessagingAdminService 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultMessagingAdminService 코드베이스 검색 — 2줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 대역으로 조립하는 비용이 낮다
|
||||
|
||||
`DefaultMessagingAdminService` 의 생성자는 10개 인자를 받고 그중 8개가 SPI 또는 `Supplier` 다. §12.1(a)의 회귀 테스트도 이 층에서 쓰는 것이 자연스럽다 — 저널·리스·리드라이브 루프가 함께 도는 것이 결함이 나타나는 조건이기 때문이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
부팅된 컨텍스트에서 admin 평면을 켜고 빈 그래프를 관측하지 않았다. 런타임 관측을 수행하지 않았다.
|
||||
|
||||
<!-- body:end -->
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-admin-runtime-f04
|
||||
title: public 인터페이스를 패키지 밖에서 구현할 수 없다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-admin-runtime-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-runtime-f04
|
||||
file: ../../../final/evidence/rendered/messaging-admin-runtime-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-runtime-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-runtime.md#L924 이다.
|
||||
module: messaging-admin-runtime
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# public 인터페이스를 패키지 밖에서 구현할 수 없다
|
||||
|
||||
RedriveEstimator(public)의 반환 타입 RedriveEstimate 가 package-private 이다(EVD-308). DefaultMessagingAdminService 의 public 생성자가 그 인터페이스를 요구하므로, 외부 조립이 불가능하다.
|
||||
|
||||
## 문제
|
||||
|
||||
RedriveEstimator(public)의 반환 타입 RedriveEstimate 가 package-private 이다(EVD-308).
|
||||
|
||||
DefaultMessagingAdminService 의 public 생성자가 그 인터페이스를 요구하므로, 외부 조립이 불가능하다.
|
||||
|
||||
## 결론
|
||||
|
||||
RedriveEstimate 를 public 으로 올리는 것이 최소 수정이다.
|
||||
|
||||
더 나은 방향은 DefaultMessagingAdminService 밖의 최상위 record 로 꺼내는 것 — 지금은 오케스트레이터의 내부 타입이 SPI 계약의 일부가 되어 있다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : RedriveEstimator 참조 3건 검색과 반환 타입 및 생성자 인자의 접근 제한자 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-admin-runtime.md#L924 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`RedriveEstimator`(public)의 반환 타입 `RedriveEstimate` 가 package-private 이다(`EVD-308`). `DefaultMessagingAdminService` 의 public 생성자가 그 인터페이스를 요구하므로, 외부 조립이 불가능하다.
|
||||
|
||||
## RedriveEstimator 참조 위치
|
||||
|
||||
:::evidence key="messaging-admin-runtime-f04" alt="코드베이스에서 RedriveEstimator 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedriveEstimator 코드베이스 검색 — 3줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 최소 수정과 더 나은 방향
|
||||
|
||||
`RedriveEstimate` 를 public 으로 올리는 것이 최소 수정이다. 더 나은 방향은 `DefaultMessagingAdminService` 밖의 최상위 record 로 꺼내는 것 — 지금은 오케스트레이터의 내부 타입이 SPI 계약의 일부가 되어 있다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
패키지 밖에서 실제로 구현을 시도해 컴파일 실패를 관측하지 않았다. 접근 제한자 조합으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-admin-runtime-f09
|
||||
title: 선언된 의존 6개 중 3개가 import 0건
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-admin-runtime-f09
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-runtime-f09
|
||||
file: ../../../final/evidence/rendered/messaging-admin-runtime-f09.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-runtime-f09.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-runtime.md#L957 이다.
|
||||
module: messaging-admin-runtime
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 선언된 의존 6개 중 3개가 import 0건
|
||||
|
||||
messaging-policy, messaging-transport-spi, messaging-security. 제거 후보.
|
||||
|
||||
## 문제
|
||||
|
||||
messaging-policy, messaging-transport-spi, messaging-security.
|
||||
|
||||
제거 후보.
|
||||
|
||||
## 결론
|
||||
|
||||
messaging-policy, messaging-transport-spi, messaging-security.
|
||||
|
||||
제거 후보.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 선언된 의존 6개에 대한 패키지 이름 import 전수 검색
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-admin-runtime.md#L957 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
선언된 의존 6개 중 3개가 import 0건이다 — `messaging-policy`, `messaging-transport-spi`, `messaging-security`.
|
||||
|
||||
## import 0 건인 세 의존
|
||||
|
||||
:::evidence key="messaging-admin-runtime-f09" alt="분석 문서 analysis/messaging/messaging-admin-runtime.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-admin-runtime.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 제거 후보
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
세 의존을 제거하고 빌드를 돌려 보지 않았다. import 0 건으로 판정했으므로 리플렉션이나 문자열을 통한 간접 사용은 배제하지 못했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-claim-check-f01
|
||||
title: 배포 아티팩트가 싣지만 아무도 부르지 않고, 다른 곳의 에러 메시지가 이 경로를 권한다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-claim-check-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-claim-check-f01
|
||||
file: ../../../final/evidence/rendered/messaging-claim-check-f01.svg
|
||||
- key: messaging-claim-check-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-claim-check-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-claim-check-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-claim-check.md#L507 이다.
|
||||
module: messaging-claim-check
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 배포 아티팩트가 싣지만 아무도 부르지 않고, 다른 곳의 에러 메시지가 이 경로를 권한다
|
||||
|
||||
여섯 타입 전부 leaf 밖 참조 0, ClaimCheckStore 구현이 테스트 fake뿐, 조립 0건. 그런데 runtime_memberships가 ["app-bootstrap"]이고 starter의 allowed_dependencies에 포함된다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
여섯 타입 전부 leaf 밖 참조 0, ClaimCheckStore 구현이 테스트 fake뿐, 조립 0건.
|
||||
|
||||
그런데 runtime_memberships가 ["app-bootstrap"]이고 starter의 allowed_dependencies에 포함된다.
|
||||
|
||||
## 결론
|
||||
|
||||
그리고 messaging-policy의 PayloadLimitGuard가 상한 초과 payload를 거절하며 "payload of %d bytes exceeds the %d byte limit for %s; use claim check"라고 안내한다.
|
||||
|
||||
운영자가 상한 초과 오류를 보고 안내대로 claim check를 켜려 해도 켤 것이 없다 — 저장소 구현도, bean도, 오프로드를 부르는 발행 경로도 없다.
|
||||
|
||||
그리고 DestinationProfile이 claimCheckThresholdBytes를 선언하고 검증까지 하므로 설정 표면은 존재한다.
|
||||
|
||||
설정할 수 있고 아무 효과가 없는 값이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : ClaimCheckStore 참조 6건 검색과 runtime_memberships 값 및 다른 리프의 오류 메시지 안내 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-claim-check.md#L507 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
여섯 타입 전부 leaf 밖 참조 0, `ClaimCheckStore` 구현이 테스트 fake뿐, 조립 0건. 그런데 `runtime_memberships`가 `["app-bootstrap"]`이고 starter의 `allowed_dependencies`에 포함된다.
|
||||
|
||||
## 안내가 가리키는 빈자리
|
||||
|
||||
:::evidence key="messaging-claim-check-f01-diagram" alt="설정 표면과 오류 메시지 안내가 운영자가 켤 수 있는 것 안에 놓이고 저장소 구현과 bean 이 바깥에 빗금으로 놓인다" caption="안내가 가리키는 빈자리" zoom="false"
|
||||
:::
|
||||
|
||||
## ClaimCheckStore 참조 위치
|
||||
|
||||
:::evidence key="messaging-claim-check-f01" alt="코드베이스에서 ClaimCheckStore 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ClaimCheckStore 코드베이스 검색 — 6줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 다른 곳의 에러 메시지가 이 경로를 권한다
|
||||
|
||||
`messaging-policy`의 `PayloadLimitGuard`가 상한 초과 payload를 거절하며 `"payload of %d bytes exceeds the %d byte limit for %s; use claim check"`라고 안내한다. 운영자가 그 안내대로 claim check를 켜려 해도 켤 것이 없다 — 저장소 구현도, bean도, 오프로드를 부르는 발행 경로도 없다.
|
||||
|
||||
## 설정 표면은 존재한다
|
||||
|
||||
`DestinationProfile`이 `claimCheckThresholdBytes`를 선언하고 검증까지 한다. 설정할 수 있고 아무 효과가 없는 값이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
ClaimCheckStore 를 구현할 계획이 있는지 확인할 수 없었다. objectstorage 어댑터가 후보이지만 두 리프가 registry 에서 연결되지 않는다.
|
||||
|
||||
<!-- body:end -->
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-cloudevents-f02
|
||||
title: 배포 아티팩트가 싣지만 아무도 부르지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-cloudevents-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-cloudevents-f02
|
||||
file: ../../../final/evidence/rendered/messaging-cloudevents-f02.svg
|
||||
- key: messaging-cloudevents-f02-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-cloudevents-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-cloudevents-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-cloudevents.md#L529 이다.
|
||||
module: messaging-cloudevents
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 배포 아티팩트가 싣지만 아무도 부르지 않는다
|
||||
|
||||
세 타입의 leaf 밖 참조가 0인데 runtime_memberships가 ["app-bootstrap"]이다. messaging-spring-boot-starter의 의존 목록에 있어 cloudevents-api·cloudevents-core 두 jar가 런타임 classpath에 오른다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
세 타입의 leaf 밖 참조가 0인데 runtime_memberships가 ["app-bootstrap"]이다.
|
||||
|
||||
messaging-spring-boot-starter의 의존 목록에 있어 cloudevents-api·cloudevents-core 두 jar가 런타임 classpath에 오른다.
|
||||
|
||||
## 결론
|
||||
|
||||
starter에 CloudEventMapper를 만드는 @Bean이 없다.
|
||||
|
||||
형제 Avro·Protobuf는 소비자 0과 membership []이 일치하는 정합적 incubating 상태다.
|
||||
|
||||
이 leaf만 어긋난다.
|
||||
|
||||
오늘 실행되는 코드가 없으므로 사고는 아니지만, 아티팩트 크기와 "이 의존성이 왜 있지"의 조사 비용이 남는다.
|
||||
|
||||
그리고 support-matrix.md:23이 "모든 messaging leaf가 unwired"라고 적고 있어 문서에서도 이 사실을 알 수 없다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : CloudEventMapper 참조 3건 검색과 배포 아티팩트에 실리는 jar·타입 목록 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-cloudevents.md#L529 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
세 타입의 leaf 밖 참조가 0인데 `runtime_memberships`가 `["app-bootstrap"]`이다.
|
||||
|
||||
## 싣고 쓰지 않는 구조
|
||||
|
||||
:::evidence key="messaging-cloudevents-f02-diagram" alt="cloudevents 두 jar 와 leaf 세 타입이 배포 아티팩트 안에 놓이고 CloudEventMapper 빈이 바깥에 빗금으로 놓인다" caption="싣고 쓰지 않는 구조" zoom="false"
|
||||
:::
|
||||
|
||||
`messaging-spring-boot-starter`의 의존 목록에 있어 `cloudevents-api`·`cloudevents-core` 두 jar가 런타임 classpath에 오른다. starter에 `CloudEventMapper`를 만드는 `@Bean`이 없다.
|
||||
|
||||
## CloudEventMapper 참조 위치
|
||||
|
||||
:::evidence key="messaging-cloudevents-f02" alt="코드베이스에서 CloudEventMapper 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CloudEventMapper 코드베이스 검색 — 3줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 형제 둘은 정합적이다
|
||||
|
||||
Avro·Protobuf는 소비자 0과 membership `[]`이 일치하는 incubating 상태다. 이 leaf만 어긋난다.
|
||||
|
||||
## 오늘 사고는 아니고 조사 비용이 남는다
|
||||
|
||||
실행되는 코드가 없으므로 사고는 아니지만, 아티팩트 크기와 "이 의존성이 왜 있지"의 조사 비용이 남는다. 그리고 `support-matrix.md:23`이 "모든 messaging leaf가 unwired"라고 적고 있어 문서에서도 이 사실을 알 수 없다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
이 리프가 starter 의존 목록에 들어간 시점과 이유를 확인할 수 없었다. 커밋이 대량 커밋 4개뿐이다.
|
||||
|
||||
<!-- body:end -->
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-inbox-jdbc-postgresql-f01
|
||||
title: bounded purge가 구현돼 있고 호출되지 않아, cleanup이 스스로 막겠다고 한 장애를 일으킨다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-inbox-jdbc-postgresql-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-inbox-jdbc-postgresql-f01
|
||||
file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-f01.svg
|
||||
- key: messaging-inbox-jdbc-postgresql-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-inbox-jdbc-postgresql-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-inbox-jdbc-postgresql.md#L647 이다.
|
||||
module: messaging-inbox-jdbc-postgresql
|
||||
priority: P1
|
||||
---
|
||||
|
||||
# bounded purge가 구현돼 있고 호출되지 않아, cleanup이 스스로 막겠다고 한 장애를 일으킨다
|
||||
|
||||
InboxRepository·OutboxRepository 둘 다 purge*Before(Instant, int) 오버로드를 선언하고, JdbcInboxRepository:141·JdbcOutboxRepository:486이 LIMIT + FOR UPDATE SKIP LOCKED로 구현한다. 저장소 전체에서 그 시그니처가 등장하는 9곳은 선언 2 + 구현 2 + 테스트 fake override 5이고 호출 지점이 0이다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
InboxRepository·OutboxRepository 둘 다 purge*Before(Instant, int) 오버로드를 선언하고, JdbcInboxRepository:141·JdbcOutboxRepository:486이 LIMIT + FOR UPDATE SKIP LOCKED로 구현한다.
|
||||
|
||||
저장소 전체에서 그 시그니처가 등장하는 9곳은 선언 2 + 구현 2 + 테스트 fake override 5이고 호출 지점이 0이다.
|
||||
|
||||
## 결론
|
||||
|
||||
InboxCleanupJob:56과 OutboxCleanupJob:50이 무제한 오버로드를 부른다.
|
||||
|
||||
InboxCleanupJob.DEFAULT_BATCH_SIZE = 1_000은 자기 선언 한 줄만 존재한다.
|
||||
|
||||
InboxCleanupJob의 javadoc이 스스로 적는다 — "A single unbounded DELETE over a table that has been accumulating for weeks holds locks long enough to block the very reservations the inbox exists to serve, so the cleanup would cause the outage it is meant to prevent." 실행되는 코드가 정확히 그 문장이 서술하는 동작이다.
|
||||
|
||||
OutboxRepository의 bounded 오버로드 javadoc은 한 발 더 나간다 — "The cleanup jobs describe themselves as bounded by batch size; this is the parameter that makes that true." 그 파라미터를 아무도 넘기지 않는다.
|
||||
|
||||
그리고 두 leaf가 동일한 형태로 그렇다.
|
||||
|
||||
왜 P1인가.
|
||||
|
||||
두 leaf 다 runtime_memberships: ["app-bootstrap"]이고 두 cleanup job이 starter에서 bean으로 만들어진다(MessagingReliabilityAutoConfiguration의 inboxCleanupJob·outboxCleanupJob). 즉 출하 구성에서 실행되는 경로이며, 백로그가 쌓인 뒤 첫 스윕에서 발현한다. 다른 미배선 발견들과 성격이 다르다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : InboxRepository 참조 20건 검색으로 bounded 시그니처가 등장하는 9곳을 선언·구현·호출로 분류
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-inbox-jdbc-postgresql.md#L647 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`InboxRepository`·`OutboxRepository` 둘 다 `purge*Before(Instant, int)` 오버로드를 선언하고, `JdbcInboxRepository:141`·`JdbcOutboxRepository:486`이 `LIMIT` + `FOR UPDATE SKIP LOCKED`로 구현한다. 저장소 전체에서 그 시그니처가 등장하는 9곳은 **선언 2 + 구현 2 + 테스트 fake override 5**이고 **호출 지점이 0**이다. `InboxCleanupJob:56`과 `OutboxCleanupJob:50`이 무제한 오버로드를 부른다.
|
||||
|
||||
## 정리가 일으키는 장애
|
||||
|
||||
:::evidence key="messaging-inbox-jdbc-postgresql-f01-diagram" alt="쌓인 백로그가 무제한 DELETE 와 락 장기 보유를 지나 예약이 막히는 결과로 이어진다" caption="정리가 일으키는 장애" zoom="false"
|
||||
:::
|
||||
|
||||
## InboxRepository 참조 위치
|
||||
|
||||
:::evidence key="messaging-inbox-jdbc-postgresql-f01" alt="코드베이스에서 InboxRepository 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxRepository 코드베이스 검색 — 20줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## javadoc 자신이 이 동작을 서술한다
|
||||
|
||||
`InboxCleanupJob`의 javadoc — "A single unbounded DELETE over a table that has been accumulating for weeks holds locks long enough to block the very reservations the inbox exists to serve, so the cleanup would cause the outage it is meant to prevent." 실행되는 코드가 정확히 그 문장이 서술하는 동작이다. `OutboxRepository`의 bounded 오버로드 javadoc은 한 발 더 나간다 — "The cleanup jobs describe themselves as bounded by batch size; **this is the parameter that makes that true**." 그 파라미터를 아무도 넘기지 않는다. `InboxCleanupJob.DEFAULT_BATCH_SIZE = 1_000`은 자기 선언 한 줄만 존재한다.
|
||||
|
||||
## 왜 P1인가
|
||||
|
||||
두 leaf 다 `runtime_memberships: ["app-bootstrap"]`이고 두 cleanup job이 starter에서 bean으로 만들어진다(`MessagingReliabilityAutoConfiguration`의 `inboxCleanupJob`·`outboxCleanupJob`). 즉 **출하 구성에서 실행되는 경로**이며, 백로그가 쌓인 뒤 첫 스윕에서 발현한다. 다른 미배선 발견들과 성격이 다르다.
|
||||
|
||||
## 후보
|
||||
|
||||
두 job이 bounded 오버로드에 배치 크기를 넘기게 한다 — `InboxCleanupJob`은 이미 `DEFAULT_BATCH_SIZE`를 갖고 있다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
무제한 DELETE 가 실제 규모의 테이블에서 얼마나 오래 락을 잡는지 측정하지 않았다.
|
||||
|
||||
<!-- body:end -->
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-observability-f02
|
||||
title: 관측 구현이 조립되지 않고, 그 재료 둘만 bean으로 존재한다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-observability-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-observability-f02
|
||||
file: ../../../final/evidence/rendered/messaging-observability-f02.svg
|
||||
- key: messaging-observability-f02-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-observability-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-observability-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-observability.md#L682 이다.
|
||||
module: messaging-observability
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 관측 구현이 조립되지 않고, 그 재료 둘만 bean으로 존재한다
|
||||
|
||||
MessagingMetrics는 MessagingObservation의 유일한 구현이고 저장소 전체에서 자기 테스트에서만 생성된다. starter는 그 생성자 인자 둘(MessagingRedactor:253, CardinalityGuard:264)을 bean으로 만들고 MessagingMetrics bean은 만들지 않는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **타입이 문서화한 불변식은 타입이 강제한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
MessagingMetrics는 MessagingObservation의 유일한 구현이고 저장소 전체에서 자기 테스트에서만 생성된다.
|
||||
|
||||
starter는 그 생성자 인자 둘(MessagingRedactor:253, CardinalityGuard:264)을 bean으로 만들고 MessagingMetrics bean은 만들지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
DefaultMessagePublisher는 NO_OBSERVATION을 쓰는 6인자 생성자로 조립된다.
|
||||
|
||||
재료·구현·seam·호출부가 전부 있고 조립 한 줄이 없다.
|
||||
|
||||
그리고 두 재료 bean은 주입처가 0이므로 컨텍스트에 앉아만 있다 — bean 존재 검사는 통과한다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : MessagingMetrics 참조 16건 검색과 starter 가 만드는 bean 목록 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-observability.md#L682 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`MessagingMetrics`는 `MessagingObservation`의 유일한 구현이고 저장소 전체에서 자기 테스트에서만 생성된다.
|
||||
|
||||
## 재료와 조립의 거리
|
||||
|
||||
:::evidence key="messaging-observability-f02-diagram" alt="MessagingRedactor 와 CardinalityGuard 가 starter 가 만드는 bean 안에 놓이고 MessagingMetrics 가 바깥에 빗금으로 놓인다" caption="재료와 조립의 거리" zoom="false"
|
||||
:::
|
||||
|
||||
starter는 그 생성자 인자 둘(`MessagingRedactor:253`, `CardinalityGuard:264`)을 bean으로 만들고 `MessagingMetrics` bean은 만들지 않는다.
|
||||
|
||||
## MessagingMetrics 참조 위치
|
||||
|
||||
:::evidence key="messaging-observability-f02" alt="코드베이스에서 MessagingMetrics 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingMetrics 코드베이스 검색 — 16줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 조립 한 줄이 없다
|
||||
|
||||
`DefaultMessagePublisher`는 `NO_OBSERVATION`을 쓰는 6인자 생성자로 조립된다. 재료·구현·seam·호출부가 전부 있다. 그리고 두 재료 bean은 주입처가 0이므로 컨텍스트에 앉아만 있다 — bean 존재 검사는 통과한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
이 bean 이 없는 것이 미완인지 확장점인지 확인하지 못했다. 저장소 안에 답이 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-outbox-jdbc-postgresql-f01
|
||||
title: 정리 작업이 무제한 DELETE 를 쏘고, 그것을 막는 오버로드는 호출되지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-outbox-jdbc-postgresql-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-outbox-jdbc-postgresql-f01
|
||||
file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-f01.svg
|
||||
- key: messaging-outbox-jdbc-postgresql-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-outbox-jdbc-postgresql-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L876 이다.
|
||||
module: messaging-outbox-jdbc-postgresql
|
||||
priority: P1
|
||||
---
|
||||
|
||||
# 정리 작업이 무제한 DELETE 를 쏘고, 그것을 막는 오버로드는 호출되지 않는다
|
||||
|
||||
OutboxCleanupJob:50 과 InboxCleanupJob:56 이 무제한 오버로드를 부른다. bounded 오버로드(purgePublishedBefore(Instant, int) / purgeProcessedBefore(Instant, int))는 두 포트에 선언되고 두 구현에 구현되어 있으며 호출부가 0건이다(EVD-294, EVD-311).
|
||||
|
||||
## 문제
|
||||
|
||||
OutboxCleanupJob:50 과 InboxCleanupJob:56 이 무제한 오버로드를 부른다.
|
||||
|
||||
bounded 오버로드(purgePublishedBefore(Instant, int) / purgeProcessedBefore(Instant, int))는 두 포트에 선언되고 두 구현에 구현되어 있으며 호출부가 0건이다(EVD-294, EVD-311).
|
||||
|
||||
## 결론
|
||||
|
||||
두 잡 모두 starter 빈이지만 스케줄러는 등록되지 않으며, 그것은 의도된 설계다(EVD-316).
|
||||
|
||||
즉 기본 배포에서는 아무 일도 일어나지 않고, 애플리케이션이 문서 지시대로 잡을 스케줄하는 순간 무제한 DELETE 가 발동한다.
|
||||
|
||||
잠재 결함이지 상시 결함이 아니다.
|
||||
|
||||
bounded 구현의 주석이 결과를 명시한다: "An unbounded DELETE holds locks and writes WAL in proportion to the whole backlog, which stalls the relay and the business writes behind retention." 3일치 백로그가 쌓인 테이블에서 이것은 릴레이 정지와 비즈니스 쓰기 정체를 뜻한다.
|
||||
|
||||
두 리프 모두 runtime_memberships: ["app-bootstrap"] 이고 두 잡 모두 starter 빈이다.
|
||||
|
||||
수정은 한 줄이다 — purgePublishedBefore(cutoff, batchLimit).
|
||||
|
||||
maxBatches 가 그제서야 의미를 갖는다.
|
||||
|
||||
배치 크기는 새 파라미터가 필요하고, OutboxProperties.batchSize(100)를 재사용하거나 별도 값을 둔다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : OutboxProperties 참조 28건 검색과 두 오버로드의 SQL·호출자·starter 배선 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L876 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`OutboxCleanupJob:50` 과 `InboxCleanupJob:56` 이 무제한 오버로드를 부른다. bounded 오버로드(`purgePublishedBefore(Instant, int)` / `purgeProcessedBefore(Instant, int)`)는 두 포트에 선언되고 두 구현에 구현되어 있으며 호출부가 **0건**이다(`EVD-294`, `EVD-311`).
|
||||
|
||||
## 두 형태의 락 구간
|
||||
|
||||
:::evidence key="messaging-outbox-jdbc-postgresql-f01-diagram" alt="호출되는 오버로드 쪽에 무제한 DELETE 와 전체 백로그 락이 빗금으로 놓이고 호출되지 않는 오버로드 쪽에 LIMIT 배치와 배치 단위 락이 놓인다" caption="두 형태의 락 구간" zoom="false"
|
||||
:::
|
||||
|
||||
## OutboxProperties 참조 위치
|
||||
|
||||
:::evidence key="messaging-outbox-jdbc-postgresql-f01" alt="코드베이스에서 OutboxProperties 를 검색한 출력 28줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxProperties 코드베이스 검색 — 28줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 잠재 결함이지 상시 결함이 아니다
|
||||
|
||||
두 잡 모두 starter 빈이지만 스케줄러는 등록되지 않으며, 그것은 의도된 설계다(`EVD-316`). 즉 기본 배포에서는 아무 일도 일어나지 않고, 애플리케이션이 문서 지시대로 잡을 스케줄하는 순간 무제한 DELETE 가 발동한다.
|
||||
|
||||
## bounded 구현의 주석이 결과를 명시한다
|
||||
|
||||
"An unbounded DELETE holds locks and writes WAL in proportion to the whole backlog, which stalls the relay and the business writes behind retention." 3일치 백로그가 쌓인 테이블에서 이것은 릴레이 정지와 비즈니스 쓰기 정체를 뜻한다. 두 리프 모두 `runtime_memberships: ["app-bootstrap"]` 이다.
|
||||
|
||||
## 수정은 한 줄이고 대역도 함께 고쳐야 한다
|
||||
|
||||
`purgePublishedBefore(cutoff, batchLimit)` 로 바꾸면 `maxBatches` 가 그제서야 의미를 갖는다. 배치 크기는 `OutboxProperties.batchSize`(100)를 재사용하거나 별도 값을 둔다. 그리고 회귀 테스트가 성립하려면 `RecordingRepository` 를 고쳐야 한다 — 현재 대역의 bounded 구현은 `Math.min(unbounded(), limit)` 로 전부 지우고 숫자만 깎는다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
백로그가 쌓인 실제 테이블에서 무제한 DELETE 의 락 보유 시간을 측정하지 않았다. 두 SQL 과 호출부 부재로 도출했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-outbox-jdbc-postgresql-f04
|
||||
title: 두 릴레이 상호배제가 기동에서 강제되지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-outbox-jdbc-postgresql-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-outbox-jdbc-postgresql-f04
|
||||
file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-f04.svg
|
||||
- key: messaging-outbox-jdbc-postgresql-f04-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-outbox-jdbc-postgresql-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L909 이다.
|
||||
module: messaging-outbox-jdbc-postgresql
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 두 릴레이 상호배제가 기동에서 강제되지 않는다
|
||||
|
||||
DebeziumOutboxProfile.requireExactlyOneRelay(...) 는 프로덕션 호출부가 0건이다. 클래스 javadoc 은 "the incompatibility is therefore enforced at startup instead of documented" 라고 쓴다.
|
||||
|
||||
## 문제
|
||||
|
||||
DebeziumOutboxProfile.requireExactlyOneRelay(...) 는 프로덕션 호출부가 0건이다.
|
||||
|
||||
클래스 javadoc 은 "the incompatibility is therefore enforced at startup instead of documented" 라고 쓴다.
|
||||
|
||||
## 결론
|
||||
|
||||
properties 파일도 같은 경고를 반복한다("Enable this OR the in-process polling relay, never both").
|
||||
|
||||
같은 리프에 정확히 이 형태를 고친 선례가 있다 — OutboxRelayWorker 가 "nothing ever called runOnce" 를 고치고 MessagingOutboxRelayLifecycle 로 배선까지 마쳤다.
|
||||
|
||||
같은 방식으로 MessagingReliabilityAutoConfiguration 에 프로필 빈과 InitializingBean 검사를 두면 된다.
|
||||
|
||||
배선하려면 CDC 모드를 선택할 프로퍼티도 필요하다 — 지금은 DebeziumOutboxProfile 을 만드는 설정 경로 자체가 없다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : OutboxRelayWorker 참조 17건 검색과 상호배제 검사 메서드의 프로덕션 호출부 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L909 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`DebeziumOutboxProfile.requireExactlyOneRelay(...)` 는 프로덕션 호출부가 0건이다. 클래스 javadoc 은 "the incompatibility is therefore enforced at startup instead of documented" 라고 쓴다.
|
||||
|
||||
## 배제가 강제되지 않는 자리
|
||||
|
||||
:::evidence key="messaging-outbox-jdbc-postgresql-f04-diagram" alt="문서의 경고만 기동에서 강제되는 것 안에 놓이고 requireExactlyOneRelay 호출이 바깥에 빗금으로 놓인다" caption="배제가 강제되지 않는 자리" zoom="false"
|
||||
:::
|
||||
|
||||
properties 파일도 같은 경고를 반복한다("Enable this OR the in-process polling relay, never both").
|
||||
|
||||
## OutboxRelayWorker 참조 위치
|
||||
|
||||
:::evidence key="messaging-outbox-jdbc-postgresql-f04" alt="코드베이스에서 OutboxRelayWorker 를 검색한 출력 17줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxRelayWorker 코드베이스 검색 — 17줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 같은 리프에 선례가 있다
|
||||
|
||||
`OutboxRelayWorker` 가 "nothing ever called `runOnce`" 를 고치고 `MessagingOutboxRelayLifecycle` 로 배선까지 마쳤다. 같은 방식으로 `MessagingReliabilityAutoConfiguration` 에 프로필 빈과 `InitializingBean` 검사를 두면 된다. 배선하려면 CDC 모드를 선택할 프로퍼티도 필요하다 — 지금은 `DebeziumOutboxProfile` 을 만드는 설정 경로 자체가 없다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
두 릴레이를 동시에 켠 배포를 만들어 관측하지 않았다. 검사 메서드의 호출부가 0 건이라는 것으로 도출했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-rabbit-f04
|
||||
title: 능력 상수의 delayedDelivery 가 무조건 참이고, 그 지연을 제공할 토폴로지는 조립되지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-rabbit-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-rabbit-f04
|
||||
file: ../../../final/evidence/rendered/messaging-rabbit-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-rabbit-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-rabbit.md#L307 이다.
|
||||
module: messaging-rabbit
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 능력 상수의 delayedDelivery 가 무조건 참이고, 그 지연을 제공할 토폴로지는 조립되지 않는다
|
||||
|
||||
그런데 지연을 실제로 만드는 것은 RabbitRetryQueueTopology 이고, 그 클래스는 자기 파일과 시험 하나 밖에서 참조되지 않는다. 어떤 production 코드도 그 큐를 선언하지 않는다.
|
||||
|
||||
## 문제
|
||||
|
||||
그런데 지연을 실제로 만드는 것은 RabbitRetryQueueTopology 이고, 그 클래스는 자기 파일과 시험 하나 밖에서 참조되지 않는다.
|
||||
|
||||
어떤 production 코드도 그 큐를 선언하지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
그리고 그 클래스의 javadoc 이 이 지연의 성질을 정확히 적는다.
|
||||
|
||||
즉 제공되는 것은 "메시지별 지연" 이 아니라 "재시도 큐 하나당 TTL 하나" 다.
|
||||
|
||||
능력 모델에는 그 구분을 표현하는 자리가 없고, 상수는 프로파일과 무관하게 참을 답한다.
|
||||
|
||||
Kafka 는 같은 칸을 false 로 둔다.
|
||||
|
||||
그래서 이 플래그의 두 값이 "지연 있음/없음" 이 아니라 "지연을 흉내낼 토폴로지를 선언할 수 있음/없음" 을 뜻하게 된다.
|
||||
|
||||
수정은 능력을 전송 상수가 아니라 목적지의 재시도 큐 선언에서 파생시키는 것이다.
|
||||
|
||||
이 리프가 조립되지 않는 동안에는 P3 이고, RabbitChannelPublisher 구현이 생기는 날 함께 봐야 한다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : RabbitRetryQueueTopology 참조 4건 검색으로 그 큐를 선언하는 production 코드 유무 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-rabbit.md#L307 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
능력 상수의 `delayedDelivery` 가 무조건 참이다. 그런데 지연을 실제로 만드는 것은 `RabbitRetryQueueTopology` 이고, 그 클래스는 자기 파일과 시험 하나 밖에서 참조되지 않는다. 어떤 production 코드도 그 큐를 선언하지 않는다.
|
||||
|
||||
## RabbitRetryQueueTopology 참조 위치
|
||||
|
||||
:::evidence key="messaging-rabbit-f04" alt="코드베이스에서 RabbitRetryQueueTopology 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RabbitRetryQueueTopology 코드베이스 검색 — 4줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 제공되는 것은 메시지별 지연이 아니다
|
||||
|
||||
그 클래스의 javadoc 이 이 지연의 성질을 정확히 적는다 — "재시도 큐 하나당 TTL 하나" 다. 능력 모델에는 그 구분을 표현하는 자리가 없고, 상수는 프로파일과 무관하게 참을 답한다. Kafka 는 같은 칸을 `false` 로 둔다.
|
||||
|
||||
## 플래그의 두 값이 다른 뜻이 된다
|
||||
|
||||
"지연 있음/없음" 이 아니라 "지연을 흉내낼 토폴로지를 선언할 수 있음/없음" 이다. 수정은 능력을 전송 상수가 아니라 목적지의 재시도 큐 선언에서 파생시키는 것이다. 이 리프가 조립되지 않는 동안에는 P3 이고, `RabbitChannelPublisher` 구현이 생기는 날 함께 봐야 한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
지연 재시도 큐 토폴로지를 실제로 선언해 보지 않았다. 참조가 자기 파일과 시험 하나뿐이라는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-reliability-api-f02
|
||||
title: fencing token 경로가 실제 데이터베이스에 대해 실행되지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-reliability-api-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-reliability-api-f02
|
||||
file: ../../../final/evidence/rendered/messaging-reliability-api-f02.svg
|
||||
- key: messaging-reliability-api-f02-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-reliability-api-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-reliability-api-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-reliability-api.md#L698 이다.
|
||||
module: messaging-reliability-api
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# fencing token 경로가 실제 데이터베이스에 대해 실행되지 않는다
|
||||
|
||||
OutboxRelay는 claimBatch/lease 기반 전이만 쓴다. OutboxPostgresIT는 leaseBatch/MessageId 기반 전이만 쓴다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **record의 `equals`를 좁히면 이유를 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
OutboxRelay는 claimBatch/lease 기반 전이만 쓴다.
|
||||
|
||||
OutboxPostgresIT는 leaseBatch/MessageId 기반 전이만 쓴다.
|
||||
|
||||
## 결론
|
||||
|
||||
신세대를 쓰는 다른 테스트는 InMemoryOutboxRepository와 RecordingRepository — SQL이 없는 fake다.
|
||||
|
||||
fencing의 정확성은 구현의 조건부 UPDATE가 영향 행 수를 정확히 세는지에 달려 있다.
|
||||
|
||||
OutboxTransitionResult.STALE_LEASE는 "its update matches zero rows"에서 나오고, 그것은 SQL의 성질이지 Java의 성질이 아니다.
|
||||
|
||||
in-memory fake는 그 SQL을 실행하지 않는다.
|
||||
|
||||
즉 이중 발행을 막는 장치가 그것을 검증할 수 있는 유일한 환경에서 실행되지 않는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : OutboxRelay 참조 27건 검색과 릴레이·컨테이너 테스트가 각각 쓰는 전이 세대 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-reliability-api.md#L698 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`OutboxRelay`는 `claimBatch`/lease 기반 전이만 쓴다. `OutboxPostgresIT`는 `leaseBatch`/`MessageId` 기반 전이만 쓴다.
|
||||
|
||||
## 검증이 닿은 범위
|
||||
|
||||
:::evidence key="messaging-reliability-api-f02-diagram" alt="in-memory fake 만 펜싱 경로를 실행하는 것 안에 놓이고 실 데이터베이스 위의 펜싱 경로가 바깥에 빗금으로 놓인다" caption="검증이 닿은 범위" zoom="false"
|
||||
:::
|
||||
|
||||
신세대를 쓰는 다른 테스트는 `InMemoryOutboxRepository`와 `RecordingRepository` — SQL이 없는 fake다.
|
||||
|
||||
## OutboxRelay 참조 위치
|
||||
|
||||
:::evidence key="messaging-reliability-api-f02" alt="코드베이스에서 OutboxRelay 를 검색한 출력 27줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxRelay 코드베이스 검색 — 27줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## fencing 의 정확성은 SQL 의 성질이다
|
||||
|
||||
구현의 조건부 UPDATE가 영향 행 수를 정확히 세는지에 달려 있다. `OutboxTransitionResult.STALE_LEASE`는 "its update matches zero rows"에서 나오고, in-memory fake는 그 SQL을 실행하지 않는다. 즉 **이중 발행을 막는 장치가 그것을 검증할 수 있는 유일한 환경에서 실행되지 않는다.**
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
fencing token SQL 이 실제 PostgreSQL 에서 정확한지 확인하지 못했다. 그것을 검증할 레인이 다른 세대를 쓴다.
|
||||
|
||||
<!-- body:end -->
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-runtime-core-f01
|
||||
title: 관측이 구현·호출부·주입 자리를 모두 갖추고도 출하에서 no-op이다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-runtime-core-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-runtime-core-f01
|
||||
file: ../../../final/evidence/rendered/messaging-runtime-core-f01.svg
|
||||
- key: messaging-runtime-core-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-runtime-core-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-runtime-core-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-runtime-core.md#L710 이다.
|
||||
module: messaging-runtime-core
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 관측이 구현·호출부·주입 자리를 모두 갖추고도 출하에서 no-op이다
|
||||
|
||||
DefaultMessagePublisher가 모든 발행 결과를 observation.recordPublish(...)로 기록하고, 관측을 "constructor argument rather than an optional decorator"로 받는다. MessagingMetrics가 MessagingObservation을 구현한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **만들어 두고 흘리지 않는 진단값은 진단이 아니다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
DefaultMessagePublisher가 모든 발행 결과를 observation.recordPublish(...)로 기록하고, 관측을 "constructor argument rather than an optional decorator"로 받는다.
|
||||
|
||||
MessagingMetrics가 MessagingObservation을 구현한다.
|
||||
|
||||
## 결론
|
||||
|
||||
그런데 출하 조립(MessagingCoreAutoConfiguration:446)은 6인자 생성자를 써서 NO_OBSERVATION을 넣고, MessagingMetrics는 저장소 전체에서 자기 테스트에서만 생성된다.
|
||||
|
||||
starter는 MessagingMetrics의 협력자 둘(MessagingRedactor:253, CardinalityGuard:264)을 bean으로 만든다.
|
||||
|
||||
이 필드의 javadoc이 정확히 이 상황을 막으려고 쓰였다 — "an unobserved publish path is how 'the dashboards were empty during the incident' happens".
|
||||
|
||||
그리고 같은 javadoc이 이전 결함을 "bean은 있고 호출 경로가 없었다"로 기록한다.
|
||||
|
||||
지금은 반대다 — 호출 경로가 있고 bean이 없다.
|
||||
|
||||
관측 결과는 같다.
|
||||
|
||||
고침이 간극을 닫은 게 아니라 반대편으로 옮겼다.
|
||||
|
||||
"decorator가 아니라 생성자 인자"라는 선택도 막지 못했는데, 인자를 기본값으로 채우는 짧은 생성자가 함께 있기 때문이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : DefaultMessagePublisher 참조 22건 검색과 출하 조립이 고르는 생성자의 인자 수 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-runtime-core.md#L710 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`DefaultMessagePublisher`가 모든 발행 결과를 `observation.recordPublish(...)`로 기록하고, 관측을 "constructor argument rather than an optional decorator"로 받는다. `MessagingMetrics`가 `MessagingObservation`을 구현한다.
|
||||
|
||||
## 조립이 되돌린 것
|
||||
|
||||
:::evidence key="messaging-runtime-core-f01-diagram" alt="MessagingMetrics 구현과 recordPublish 호출부와 생성자 인자 자리가 갖춰진 것 안에 놓이고 출하 조립의 6인자 생성자가 바깥에 빗금으로 놓인다" caption="조립이 되돌린 것" zoom="false"
|
||||
:::
|
||||
|
||||
그런데 출하 조립(`MessagingCoreAutoConfiguration:446`)은 **6인자 생성자**를 써서 `NO_OBSERVATION`을 넣고, `MessagingMetrics`는 저장소 전체에서 자기 테스트에서만 생성된다.
|
||||
|
||||
## DefaultMessagePublisher 참조 위치
|
||||
|
||||
:::evidence key="messaging-runtime-core-f01" alt="코드베이스에서 DefaultMessagePublisher 를 검색한 출력 22줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultMessagePublisher 코드베이스 검색 — 22줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 협력자 둘은 bean 으로 만들어진다
|
||||
|
||||
starter가 `MessagingRedactor:253`, `CardinalityGuard:264` 를 만든다. 이 필드의 javadoc이 정확히 이 상황을 막으려고 쓰였다 — "an unobserved publish path is how 'the dashboards were empty during the incident' happens".
|
||||
|
||||
## 고침이 간극을 반대편으로 옮겼다
|
||||
|
||||
같은 javadoc이 이전 결함을 "bean은 있고 호출 경로가 없었다"로 기록한다. 지금은 반대다 — 호출 경로가 있고 bean이 없다. 관측 결과는 같다. "decorator가 아니라 생성자 인자"라는 선택도 막지 못했는데, 인자를 기본값으로 채우는 짧은 생성자가 함께 있기 때문이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
6인자 생성자 선택이 의도인지 확인할 수 없었다. 커밋이 대량 커밋 4개뿐이고 이 선택을 설명하는 기록이 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-runtime-core-f02
|
||||
title: 소비 오케스트레이터가 조립되지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-runtime-core-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-runtime-core-f02
|
||||
file: ../../../final/evidence/rendered/messaging-runtime-core-f02.svg
|
||||
- key: messaging-runtime-core-f02-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-runtime-core-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-runtime-core-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-runtime-core.md#L719 이다.
|
||||
module: messaging-runtime-core
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 소비 오케스트레이터가 조립되지 않는다
|
||||
|
||||
DefaultDeliveryProcessor는 leaf 밖 참조 0, src/main 생성 0, src/test 생성 1이다. 이 클래스가 고친 문제("각 어댑터가 retry/dead-letter의 뜻을 각자 결정")가 배선 없이는 그대로 남는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **만들어 두고 흘리지 않는 진단값은 진단이 아니다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
DefaultDeliveryProcessor는 leaf 밖 참조 0, src/main 생성 0, src/test 생성 1이다.
|
||||
|
||||
이 클래스가 고친 문제("각 어댑터가 retry/dead-letter의 뜻을 각자 결정")가 배선 없이는 그대로 남는다.
|
||||
|
||||
## 결론
|
||||
|
||||
그리고 DeclaredDestinationAccess가 consume 권한을 빈 집합으로 두는 것과 정합적이다 — 기본 구성은 소비를 상정하지 않는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : DefaultDeliveryProcessor 참조 7건 검색으로 leaf 밖 참조와 src/main 생성 수 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-runtime-core.md#L719 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`DefaultDeliveryProcessor`는 leaf 밖 참조 0, `src/main` 생성 0, `src/test` 생성 1이다.
|
||||
|
||||
## 경로가 열리지 않는 이유
|
||||
|
||||
:::evidence key="messaging-runtime-core-f02-diagram" alt="발행 경로만 조립되는 것 안에 놓이고 DefaultDeliveryProcessor 가 바깥에 빗금으로 놓인다" caption="경로가 열리지 않는 이유" zoom="false"
|
||||
:::
|
||||
|
||||
## DefaultDeliveryProcessor 참조 위치
|
||||
|
||||
:::evidence key="messaging-runtime-core-f02" alt="코드베이스에서 DefaultDeliveryProcessor 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultDeliveryProcessor 코드베이스 검색 — 7줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 이 클래스가 고친 문제가 그대로 남는다
|
||||
|
||||
"각 어댑터가 retry/dead-letter의 뜻을 각자 결정" 이다. 그리고 `DeclaredDestinationAccess`가 consume 권한을 빈 집합으로 두는 것과 정합적이다 — 기본 구성은 소비를 상정하지 않는다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
파생 프로젝트가 이 오케스트레이터를 직접 조립하는지 확인할 방법이 이 저장소 안에 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-schema-api-f01
|
||||
title: 포맷 독립 진화 규칙이 호출되지 않고, 그것이 막으려던 중복이 실제로 생겼다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-schema-api-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-api-f01
|
||||
file: ../../../final/evidence/rendered/messaging-schema-api-f01.svg
|
||||
- key: messaging-schema-api-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-schema-api-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-api-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-api.md#L494 이다.
|
||||
module: messaging-schema-api
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 포맷 독립 진화 규칙이 호출되지 않고, 그것이 막으려던 중복이 실제로 생겼다
|
||||
|
||||
SchemaCompatibilityValidator의 저장소 전체 참조가 자기 선언과 자기 테스트뿐이다. 동시에 AvroCompatibilityGate가 isTransitive를 글자 그대로 복사했고 방향 판정 둘은 허용목록/거부목록으로 형태가 반대다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **port 계약은 동시성 요구를 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **도달성 판정은 단어가 아니라 import로 확인한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
SchemaCompatibilityValidator의 저장소 전체 참조가 자기 선언과 자기 테스트뿐이다.
|
||||
|
||||
동시에 AvroCompatibilityGate가 isTransitive를 글자 그대로 복사했고 방향 판정 둘은 허용목록/거부목록으로 형태가 반대다.
|
||||
|
||||
## 결론
|
||||
|
||||
오늘은 7개 모드 전부에서 두 구현의 결과가 같다(NONE_EXPERIMENTAL은 gate의 early return이 가린다).
|
||||
|
||||
그러나 enum에 값이 하나 추가되면 허용목록은 "검사 안 함", 거부목록은 "양방향 검사"로 반대 방향 기본값을 갖는다.
|
||||
|
||||
그리고 requireProductionMode — 검사 없는 스키마가 보존 로그를 뒷받침하는 것을 막는 게이트 — 는 호출되는 곳이 없다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : SchemaCompatibilityValidator 참조 11건 검색과 Avro 게이트의 중복 구현 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-schema-api.md#L494 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`SchemaCompatibilityValidator`의 저장소 전체 참조가 자기 선언과 자기 테스트뿐이다.
|
||||
|
||||
## 규칙이 막지 못한 것
|
||||
|
||||
:::evidence key="messaging-schema-api-f01-diagram" alt="자기 테스트만 규칙을 부르는 것 안에 놓이고 production 호출자가 바깥에 빗금으로 놓인다" caption="규칙이 막지 못한 것" zoom="false"
|
||||
:::
|
||||
|
||||
동시에 `AvroCompatibilityGate`가 `isTransitive`를 글자 그대로 복사했고 방향 판정 둘은 허용목록/거부목록으로 형태가 반대다.
|
||||
|
||||
## SchemaCompatibilityValidator 참조 위치
|
||||
|
||||
:::evidence key="messaging-schema-api-f01" alt="코드베이스에서 SchemaCompatibilityValidator 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SchemaCompatibilityValidator 코드베이스 검색 — 11줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 오늘은 결과가 같다
|
||||
|
||||
7개 모드 전부에서 두 구현의 결과가 같다(`NONE_EXPERIMENTAL`은 gate의 early return이 가린다). 그러나 enum에 값이 하나 추가되면 허용목록은 "검사 안 함", 거부목록은 "양방향 검사"로 **반대 방향** 기본값을 갖는다.
|
||||
|
||||
## 게이트 하나도 호출되지 않는다
|
||||
|
||||
`requireProductionMode` — 검사 없는 스키마가 보존 로그를 뒷받침하는 것을 막는 게이트 — 를 부르는 곳이 없다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
port 구현의 스레드 안전성 요구를 관측할 대상이 없다. javadoc 에 없고 이 저장소에 production 구현이 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-security-f04
|
||||
title: 종료 시 자격증명 소거가 호출되지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-security-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-security-f04
|
||||
file: ../../../final/evidence/rendered/messaging-security-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-security-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-security.md#L661 이다.
|
||||
module: messaging-security
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 종료 시 자격증명 소거가 호출되지 않는다
|
||||
|
||||
CredentialRuntimeRegistry.clearAll()의 javadoc이 "Clears every held credential, for shutdown"이라고 하고, 호출자가 저장소에 없다. 이 leaf 전체가 "비밀이 힙에 남지 않게 한다"를 목적으로 하고(char[], clear(), 회전 시 즉시 소거), 종료 경로에서 그 마지막 단계가 빠져 있다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **배선된 게이트는 자기 leaf 레인에서 검증한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
CredentialRuntimeRegistry.clearAll()의 javadoc이 "Clears every held credential, for shutdown"이라고 하고, 호출자가 저장소에 없다.
|
||||
|
||||
이 leaf 전체가 "비밀이 힙에 남지 않게 한다"를 목적으로 하고(char[], clear(), 회전 시 즉시 소거), 종료 경로에서 그 마지막 단계가 빠져 있다.
|
||||
|
||||
## 결론
|
||||
|
||||
프로세스가 끝나면 힙도 사라지지만, 종료가 느리거나 힙 덤프가 뜨는 경우가 정확히 이 통제가 노리는 상황이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : clearAll 호출자 전수 검색과 종료 계약의 자격증명 소거 단계 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-security.md#L661 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`CredentialRuntimeRegistry.clearAll()`의 javadoc이 "Clears every held credential, for shutdown"이라고 하고, 호출자가 저장소에 없다.
|
||||
|
||||
## clearAll() 의 javadoc 과 호출자 수
|
||||
|
||||
:::evidence key="messaging-security-f04" alt="분석 문서 analysis/messaging/messaging-security.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-security.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 이 leaf 전체의 목적에서 마지막 단계가 빠졌다
|
||||
|
||||
"비밀이 힙에 남지 않게 한다"를 위해 `char[]`, `clear()`, 회전 시 즉시 소거를 두었는데 종료 경로가 비어 있다.
|
||||
|
||||
## 프로세스가 끝나면 힙도 사라지지만
|
||||
|
||||
종료가 느리거나 힙 덤프가 뜨는 경우가 정확히 이 통제가 노리는 상황이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
힙 덤프로 자격증명 잔존을 확인하지 않았다. 호출자 부재로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-spring-boot-starter-f01
|
||||
title: 같은 자동 설정 안에서 검증기 하나만 감싸이지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-spring-boot-starter-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-spring-boot-starter-f01
|
||||
file: ../../../final/evidence/rendered/messaging-spring-boot-starter-f01.svg
|
||||
- key: messaging-spring-boot-starter-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-spring-boot-starter-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-spring-boot-starter-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-spring-boot-starter.md#L281 이다.
|
||||
module: messaging-spring-boot-starter
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 같은 자동 설정 안에서 검증기 하나만 감싸이지 않는다
|
||||
|
||||
KafkaMessagingAutoConfiguration 은 검증기 셋을 만든다. KafkaTransactionProfileValidator 에는 대응하는 StartupProfileValidation 이 없다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **검증기는 발행이 아니라 주입이 강제다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
KafkaMessagingAutoConfiguration 은 검증기 셋을 만든다.
|
||||
|
||||
KafkaTransactionProfileValidator 에는 대응하는 StartupProfileValidation 이 없다.
|
||||
|
||||
## 결론
|
||||
|
||||
즉 컨텍스트가 그 검증기를 발행하고 아무도 주입하지 않는다 — StartupProfileValidation 의 javadoc 이 서술한 이전 상태와 정확히 같은 형태다.
|
||||
|
||||
RabbitMessagingAutoConfiguration 은 검증기 하나이고 그것을 감싼다.
|
||||
|
||||
그러므로 이 가족에서 감싸이지 않은 검증기는 이 하나다.
|
||||
|
||||
트랜잭션 프로파일 검증이 무엇을 막는지는 그 클래스가 안다 — 비트랜잭션 생산자 위의 정확히 한 번 주장 같은 조합이다.
|
||||
|
||||
그 검증이 지금 돌지 않는다.
|
||||
|
||||
수정은 한 블록이다.
|
||||
|
||||
같은 파일의 kafkaProfileStartupValidation 형태를 복사해 세 번째 검증기를 감싼다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : KafkaMessagingAutoConfiguration 참조 6건 검색과 세 검증기의 감싸기 여부 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-spring-boot-starter.md#L281 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`KafkaMessagingAutoConfiguration` 은 검증기 셋을 만든다. `KafkaTransactionProfileValidator` 에는 대응하는 `StartupProfileValidation` 이 없다.
|
||||
|
||||
## 한 곳만 빠진 감싸기
|
||||
|
||||
:::evidence key="messaging-spring-boot-starter-f01-diagram" alt="KafkaProfileValidator 와 RabbitProfileValidator 가 감싸인 것 안에 놓이고 KafkaTransactionProfileValidator 가 바깥에 빗금으로 놓인다" caption="한 곳만 빠진 감싸기" zoom="false"
|
||||
:::
|
||||
|
||||
즉 컨텍스트가 그 검증기를 발행하고 아무도 주입하지 않는다 — `StartupProfileValidation` 의 javadoc 이 서술한 이전 상태와 정확히 같은 형태다.
|
||||
|
||||
## KafkaMessagingAutoConfiguration 참조 위치
|
||||
|
||||
:::evidence key="messaging-spring-boot-starter-f01" alt="코드베이스에서 KafkaMessagingAutoConfiguration 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="KafkaMessagingAutoConfiguration 코드베이스 검색 — 6줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 이 가족에서 감싸이지 않은 검증기는 이 하나다
|
||||
|
||||
`RabbitMessagingAutoConfiguration` 은 검증기 하나이고 그것을 감싼다. 트랜잭션 프로파일 검증이 무엇을 막는지는 그 클래스가 안다 — 비트랜잭션 생산자 위의 정확히 한 번 주장 같은 조합이다.
|
||||
|
||||
## 수정은 한 블록이다
|
||||
|
||||
같은 파일의 `kafkaProfileStartupValidation` 형태를 복사해 세 번째 검증기를 감싼다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
TLS·SASL 을 요구하는 실제 브로커에 붙여 재현하지 않았다. 조립되는 설정 맵 성분과 보안 설정기가 만드는 성분의 교집합이 0 이라는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-testkit-f01
|
||||
title: FaultController 의 5개 중 2개가 구현만 3벌 있고 호출부가 0건이다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-testkit-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-testkit-f01
|
||||
file: ../../../final/evidence/rendered/messaging-testkit-f01.svg
|
||||
- key: messaging-testkit-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-testkit-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-testkit-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-testkit.md#L940 이다.
|
||||
module: messaging-testkit
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# FaultController 의 5개 중 2개가 구현만 3벌 있고 호출부가 0건이다
|
||||
|
||||
rejectPublish() 와 reset() 은 인터페이스에 선언되어 세 하니스가 전부 구현하지만, 계약 스위트를 포함해 어디에서도 호출되지 않는다(EVD-299). rejectPublish 는 심지어 세 하니스의 publish() 경로에 완전히 배선되어 있다(KafkaContractHarness:119, RabbitContractHarness:85, InMemoryMessagingHarness:66) — 켜는 스위치만 아무도 누르지 않는다.
|
||||
|
||||
## 문제
|
||||
|
||||
rejectPublish() 와 reset() 은 인터페이스에 선언되어 세 하니스가 전부 구현하지만, 계약 스위트를 포함해 어디에서도 호출되지 않는다(EVD-299).
|
||||
|
||||
rejectPublish 는 심지어 세 하니스의 publish() 경로에 완전히 배선되어 있다(KafkaContractHarness:119, RabbitContractHarness:85, InMemoryMessagingHarness:66) — 켜는 스위치만 아무도 누르지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
이것이 단순한 미사용 코드가 아닌 이유: 미사용 경로가 틀린 값을 인코딩하고 있다.
|
||||
|
||||
InMemoryMessagingHarness 에서 rejectPublish 는 rejected("BROKER_REJECTED", …) 를 돌려주고, 그 헬퍼는 PublishEvidence.notTransmitted() 를 쓴다(:184-193).
|
||||
|
||||
TransmissionEvidence.NOT_TRANSMITTED 의 javadoc 은 "Nothing was written to the broker connection." 이다.
|
||||
|
||||
그런데 FaultController.rejectPublish 의 javadoc 은 "refused outright by the broker" 다 — 브로커가 거절하려면 바이트가 나갔어야 하므로 TRANSMITTED 여야 한다.
|
||||
|
||||
이 플랫폼은 전송 증거를 세 값으로 구분하는 것을 핵심 가치로 삼는데, 유일하게 실행되지 않는 경로에 그 구분의 오류가 들어 있다.
|
||||
|
||||
connection-refused 시나리오(유일하게 증거가 없는 시나리오, Expectation.REJECTED)와 이 미사용 결함이 같은 빈칸을 가리킨다.
|
||||
|
||||
둘 중 하나를 택해야 한다: 계약에 rejectsWhenBrokerRefusesBeforeTransmission 를 추가하고 전송 증거를 바로잡거나, rejectPublish 를 인터페이스에서 제거해 세 하니스의 구현 부담을 없애거나.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : FaultController 참조 19건 검색과 세 하니스의 구현·배선 지점 대비 호출부 수 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-testkit.md#L940 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`rejectPublish()` 와 `reset()` 은 인터페이스에 선언되어 세 하니스가 전부 구현하지만, 계약 스위트를 포함해 어디에서도 호출되지 않는다(`EVD-299`).
|
||||
|
||||
## 구현만 있는 고장 종류
|
||||
|
||||
:::evidence key="messaging-testkit-f01-diagram" alt="세 하니스의 구현과 publish 경로 배선이 고장 종류에 있는 것 안에 놓이고 호출부가 바깥에 빗금으로 놓인다" caption="구현만 있는 고장 종류" zoom="false"
|
||||
:::
|
||||
|
||||
`rejectPublish` 는 심지어 세 하니스의 `publish()` 경로에 완전히 배선되어 있다(`KafkaContractHarness:119`, `RabbitContractHarness:85`, `InMemoryMessagingHarness:66`) — 켜는 스위치만 아무도 누르지 않는다.
|
||||
|
||||
## FaultController 참조 위치
|
||||
|
||||
:::evidence key="messaging-testkit-f01" alt="코드베이스에서 FaultController 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="FaultController 코드베이스 검색 — 19줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 미사용 경로가 틀린 값을 인코딩하고 있다
|
||||
|
||||
`InMemoryMessagingHarness` 에서 `rejectPublish` 는 `rejected("BROKER_REJECTED", …)` 를 돌려주고, 그 헬퍼는 `PublishEvidence.notTransmitted()` 를 쓴다(`:184-193`). `TransmissionEvidence.NOT_TRANSMITTED` 의 javadoc 은 "Nothing was written to the broker connection." 인데, `FaultController.rejectPublish` 의 javadoc 은 "refused outright by **the broker**" 다 — 브로커가 거절하려면 바이트가 나갔어야 하므로 `TRANSMITTED` 여야 한다.
|
||||
|
||||
## 유일하게 실행되지 않는 경로에 그 구분의 오류가 있다
|
||||
|
||||
이 플랫폼은 전송 증거를 세 값으로 구분하는 것을 핵심 가치로 삼는다. `connection-refused` 시나리오(유일하게 증거가 없는 시나리오, `Expectation.REJECTED`)와 이 미사용 결함이 같은 빈칸을 가리킨다. 둘 중 하나를 택해야 한다 — 계약에 `rejectsWhenBrokerRefusesBeforeTransmission` 를 추가하고 전송 증거를 바로잡거나, `rejectPublish` 를 인터페이스에서 제거해 세 하니스의 구현 부담을 없애거나.
|
||||
|
||||
## reset 은 별개다
|
||||
|
||||
세 구현 모두 결함 플래그를 one-shot 으로 소비하므로(`consumeXxx` 가 읽고 즉시 false) 리셋이 필요 없는 구조다. 계약이 테스트마다 새 하니스를 만드는 것도 같은 이유다. 제거 후보다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
두 고장을 실제로 주입해 계약 스위트가 어떻게 반응하는지 관측하지 않았다. 호출부가 0 건이라는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-testkit-f04
|
||||
title: 1 MiB 한도가 PayloadPolicy 를 두고 리터럴로 재선언된다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-testkit-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-testkit-f04
|
||||
file: ../../../final/evidence/rendered/messaging-testkit-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-testkit-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-testkit.md#L966 이다.
|
||||
module: messaging-testkit
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 1 MiB 한도가 PayloadPolicy 를 두고 리터럴로 재선언된다
|
||||
|
||||
PayloadPolicy.DEFAULT_MAX_BYTES = 1_048_576 이 정본인데 같은 값이 최소 8곳에 다시 있고(§12.3(b)), 그중 둘이 이 리프다(InMemoryMessagingHarness:31, ContractMessage:50). messaging-testkit 은 api project(':messaging:messaging-policy') 를 이미 선언하고 있으므로 import 한 줄이면 된다.
|
||||
|
||||
## 문제
|
||||
|
||||
PayloadPolicy.DEFAULT_MAX_BYTES = 1_048_576 이 정본인데 같은 값이 최소 8곳에 다시 있고(§12.3(b)), 그중 둘이 이 리프다(InMemoryMessagingHarness:31, ContractMessage:50).
|
||||
|
||||
messaging-testkit 은 api project(':messaging:messaging-policy') 를 이미 선언하고 있으므로 import 한 줄이면 된다.
|
||||
|
||||
## 결론
|
||||
|
||||
지금은 messaging-policy 의존이 import 0건이라 선언만 남아 있는데(§12.4(b)), 이 자리가 그 의존이 실제로 쓰여야 할 곳이다.
|
||||
|
||||
ContractMessage.oversized() 의 1_048_577 은 PayloadPolicy.DEFAULT_MAX_BYTES + 1 로 쓰면 "한도 바로 위 한 바이트" 라는 의도가 코드에 드러난다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : PayloadPolicy 참조 25건 검색과 같은 값이 리터럴로 재선언된 지점 집계
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-testkit.md#L966 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`PayloadPolicy.DEFAULT_MAX_BYTES = 1_048_576` 이 정본인데 같은 값이 최소 8곳에 다시 있고(§12.3(b)), 그중 둘이 이 리프다(`InMemoryMessagingHarness:31`, `ContractMessage:50`).
|
||||
|
||||
## PayloadPolicy 참조 위치
|
||||
|
||||
:::evidence key="messaging-testkit-f04" alt="코드베이스에서 PayloadPolicy 를 검색한 출력 25줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PayloadPolicy 코드베이스 검색 — 25줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## import 한 줄이면 된다
|
||||
|
||||
`messaging-testkit` 은 `api project(':messaging:messaging-policy')` 를 이미 선언하고 있다. 지금은 그 의존이 import 0건이라 선언만 남아 있는데(§12.4(b)), 이 자리가 그 의존이 실제로 쓰여야 할 곳이다.
|
||||
|
||||
## 의도가 코드에 드러나게 하는 방법
|
||||
|
||||
`ContractMessage.oversized()` 의 `1_048_577` 은 `PayloadPolicy.DEFAULT_MAX_BYTES + 1` 로 쓰면 "한도 바로 위 한 바이트" 가 읽힌다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
리터럴을 정본 상수로 바꿔 빌드를 돌려 보지 않았다. 값이 같다는 것과 의존 선언이 이미 존재한다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-testkit-f05
|
||||
title: messaging-transport-spi 의존이 import 0건이다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-testkit-f05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-testkit-f05
|
||||
file: ../../../final/evidence/rendered/messaging-testkit-f05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-testkit-f05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-testkit.md#L972 이다.
|
||||
module: messaging-testkit
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# messaging-transport-spi 의존이 import 0건이다
|
||||
|
||||
policy 와 달리 transport-spi 는 쓸 자리가 보이지 않는다. 제거 후보.
|
||||
|
||||
## 문제
|
||||
|
||||
policy 와 달리 transport-spi 는 쓸 자리가 보이지 않는다.
|
||||
|
||||
제거 후보.
|
||||
|
||||
## 결론
|
||||
|
||||
policy 와 달리 transport-spi 는 쓸 자리가 보이지 않는다.
|
||||
|
||||
제거 후보.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 선언된 의존에 대한 패키지 이름 import 전수 검색
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-testkit.md#L972 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`messaging-transport-spi` 의존이 import 0건이다.
|
||||
|
||||
## import 0 건인 의존
|
||||
|
||||
:::evidence key="messaging-testkit-f05" alt="분석 문서 analysis/messaging/messaging-testkit.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-testkit.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## policy 와 다르다
|
||||
|
||||
policy 쪽은 쓸 자리가 분명한데(§P3의 1 MiB 한도) transport-spi 는 쓸 자리가 보이지 않는다. 제거 후보.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
의존을 제거하고 빌드를 돌려 보지 않았다. import 0 건으로 판정했으므로 간접 사용은 배제하지 못했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-testkit-f06
|
||||
title: BrokerFailureMatrix.adapters() 는 호출부가 0건이다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-testkit-f06
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-testkit-f06
|
||||
file: ../../../final/evidence/rendered/messaging-testkit-f06.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-testkit-f06.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-testkit.md#L976 이다.
|
||||
module: messaging-testkit
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# BrokerFailureMatrix.adapters() 는 호출부가 0건이다
|
||||
|
||||
public 메서드이나 아무도 쓰지 않는다. 이 리프의 다른 public 표면은 전부 소비자가 있다.
|
||||
|
||||
## 문제
|
||||
|
||||
public 메서드이나 아무도 쓰지 않는다.
|
||||
|
||||
이 리프의 다른 public 표면은 전부 소비자가 있다.
|
||||
|
||||
## 결론
|
||||
|
||||
제거하거나, 진단용이라면 그렇게 적는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : adapters() 호출부 검색과 이 리프의 다른 public 표면의 소비자 유무 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-testkit.md#L976 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`BrokerFailureMatrix.adapters()` 는 public 메서드이나 아무도 쓰지 않는다.
|
||||
|
||||
## adapters() 의 호출부
|
||||
|
||||
:::evidence key="messaging-testkit-f06" alt="분석 문서 analysis/messaging/messaging-testkit.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-testkit.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 이 리프의 다른 public 표면은 전부 소비자가 있다
|
||||
|
||||
제거하거나, 진단용이라면 그렇게 적는다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
리플렉션이나 서비스 로더로 부르는 형태는 배제하지 못했다. 이름 기반 검색으로만 확인했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: startup-validator-is-the-only-reader-of-four-keys
|
||||
title: 시작 검증기가 유일한 소비자인 설정 키 넷이 아무것도 게이트하지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:startup-validator-is-the-only-reader-of-four-keys
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
body: case-startup-validator-is-the-only-reader-of-four-keys.body.md
|
||||
assets:
|
||||
- key: startup-validator-is-the-only-reader-of-four-keys
|
||||
file: ../../../final/evidence/rendered/startup-validator-is-the-only-reader-of-four-keys.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/startup-validator-is-the-only-reader-of-four-keys.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-spring-boot-starter.md#L139 이다.
|
||||
---
|
||||
|
||||
# 시작 검증기가 유일한 소비자인 설정 키 넷이 아무것도 게이트하지 않는다
|
||||
|
||||
검증기를 이름으로 부르는 파일은 자기 자신과 자기 테스트뿐이다. 자동 설정은 빈 아홉을 만들고 검증을 부르지 않는다. 그리고 그 검증기가 유일한 소비자인 설정 키가 넷이다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **시작 검증기가 시작 시 실행되지 않는다**
|
||||
다른 가족의 같은 형태다.
|
||||
- **검증기는 발행이 아니라 주입이 강제다**
|
||||
이 사례에서 뽑은 규칙이다.
|
||||
- **같은 자동 설정 안에서 검증기 하나만 감싸이지 않아 트랜잭션 조건이 검사되지 않는다**
|
||||
같은 통독에서 나온 짝이다.
|
||||
|
||||
## 문제
|
||||
|
||||
시작 검증기의 자바독이 선정 기준을 적는다.
|
||||
|
||||
여기 담긴 모든 규칙은 실행 시 증상이 침묵이거나 오귀인인 실수라는 것이다. 마감 없는 단항 메서드는 클라이언트 자신의 마감까지 매달리고, 무제한 실행기는 과부하를 무제한 지연으로 바꾸고, 운영의 전체 신뢰는 전송 보안을 보고하면서 제공하지 않고, 운영의 반사 공개는 스키마를 게시하고, 원장 없는 키 필수 메서드는 지킬 수 없는 멱등 키를 받아들인다는 것이다.
|
||||
|
||||
그리고 어느 것도 연기 테스트를 실패시키지 않는다는 것이다.
|
||||
|
||||
## 결론
|
||||
|
||||
그 검증이 돌지 않는다.
|
||||
|
||||
검증기를 이름으로 부르는 파일이 둘뿐이다. 자기 자신과 자기 테스트다.
|
||||
|
||||
자동 설정은 실행기 프로파일과 서버 프로파일과 승인 제어기와 상태 레지스트리와 반사 정책과 관리 노출 정책과 배수 정책과 문맥 결속기와 오류 사상기, 아홉 빈을 만든다. 검증 호출이 없고 초기화 콜백도 없다.
|
||||
|
||||
그래서 클래스 자바독이 약속한 성질이 성립하지 않는다. 아무도 눈치채지 못할 방식으로 틀린 설정에서 시작을 거부한다는 것이다. 지금은 그냥 시작한다.
|
||||
|
||||
함께 사라지는 것이 있다.
|
||||
|
||||
검증기가 유일한 소비자인 설정 키가 넷이다. 전송과 TLS 사용 여부와 전체 신뢰와 원장 활성화다.
|
||||
|
||||
전송 키는 자동 설정이 아예 보지 않는다. 서버 프로파일 팩토리가 안정 전송을 하드코딩한다.
|
||||
|
||||
TLS 두 키는 배포 환경의 바닥을 강제할 곳이 없다.
|
||||
|
||||
원장 키는 멱등 키 필수 메서드가 원장 없이 열리는 것을 막을 곳이 없다.
|
||||
|
||||
같은 저장소가 정본을 둘 갖고 있다. 메시징 가족은 시작 프로파일 검증을 초기화 콜백으로 돌려 이 문제를 이미 한 번 해결했고, 파일 서버 하위 트리는 증명 메서드를 부트스트랩의 빈으로 연결했다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
Spring Boot : 4.0.8
|
||||
확인 방식 : 호출자 전수 검색과 설정 키별 소비자 계수
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 document-detail 의 analysis/grpc/grpc-spring-boot-starter.md 에 있다.
|
||||
|
||||
1. 검증기 클래스 이름을 저장소 전체에서 검색한다.
|
||||
2. 자동 설정의 빈 목록을 읽고 검증 호출이 있는지 본다.
|
||||
3. 설정 속성의 각 접근자를 저장소 전체에서 검색한다.
|
||||
4. 검증기 밖에 소비자가 없는 키를 가려낸다.
|
||||
5. 서버 프로파일 팩토리가 전송 키를 읽는지 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
검증기를 이름으로 부르는 파일은 자기 자신과 자기 테스트뿐이다. 자동 설정은 빈 아홉 개를 만들고 `requireValid` 를 부르지 않으며 초기화 콜백도 없다.
|
||||
|
||||
## StartupProfileValidation 참조 위치
|
||||
|
||||
:::evidence key="startup-validator-is-the-only-reader-of-four-keys" alt="코드베이스에서 StartupProfileValidation 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="StartupProfileValidation 코드베이스 검색 — 4줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 그 검증기가 유일한 소비자인 키 넷
|
||||
|
||||
`transport`·`tls-enabled`·`trust-all-certificates`·`operation-ledger-enabled`. 따라서 운영 환경의 TLS 바닥도, 비운영 전송 거부도, 멱등 키 필수 메서드의 원장 요구도 강제되지 않는다.
|
||||
|
||||
## 같은 저장소가 정본을 둘 갖고 있다
|
||||
|
||||
messaging 의 `StartupProfileValidation` 과 fileserver 의 증명 호출이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
스타터를 애플리케이션에 올려 컨텍스트를 세우지 않았다. 빌드 전용 리프라 그 배포가 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: validator-declared-and-never-injected
|
||||
title: 같은 자동 설정 안에서 검증기 하나만 감싸이지 않아 트랜잭션 조건이 검사되지 않는다
|
||||
topic: runtime-reachability-and-composition
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:validator-declared-and-never-injected
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
body: case-validator-declared-and-never-injected.body.md
|
||||
assets:
|
||||
- key: validator-declared-and-never-injected
|
||||
file: ../../../final/evidence/rendered/validator-declared-and-never-injected.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/validator-declared-and-never-injected.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-spring-boot-starter.md#L148 이다.
|
||||
---
|
||||
|
||||
# 같은 자동 설정 안에서 검증기 하나만 감싸이지 않아 트랜잭션 조건이 검사되지 않는다
|
||||
|
||||
한 자동 설정이 검증기 둘을 만든다. 하나는 시작 검증 도우미로 감싸여 컨텍스트 구성 중에 돌고, 다른 하나는 빈으로 발행만 된다. 그 도우미의 자바독이 서술한 이전 결함이 정확히 그 형태다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **시작 검증기가 유일한 소비자인 설정 키 넷이 아무것도 게이트하지 않는다**
|
||||
같은 통독에서 나온 짝이다.
|
||||
- **검증기는 발행이 아니라 주입이 강제다**
|
||||
이 사례에서 뽑은 규칙이다.
|
||||
- **능력 상수가 프로파일 조건보다 오래 살아서 중복 제거 없는 목적지가 가드를 통과한다**
|
||||
같은 어댑터 계열의 짝이 되는 사례다.
|
||||
|
||||
## 문제
|
||||
|
||||
시작 검증 도우미가 이 가족에서 이미 한 번 고쳐진 결함을 자바독에 기록한다.
|
||||
|
||||
브로커 두 종과 보안 검증기가 전부 빈이었는데 아무 데도 주입되지 않았다는 것이다. 컨텍스트가 브로커마다 검증기를 발행하고 아무것도 검증하지 않았다는 것이다.
|
||||
|
||||
그 수정이 적용된 뒤의 상태를 확인했다.
|
||||
|
||||
## 결론
|
||||
|
||||
같은 자동 설정 안에서 하나가 빠져 있다.
|
||||
|
||||
브로커 프로파일 검증기는 도우미로 감싸여 초기화 콜백에서 돈다.
|
||||
|
||||
같은 파일의 트랜잭션 프로파일 검증기는 빈으로 발행만 된다. 대응하는 도우미 선언이 없다.
|
||||
|
||||
다른 브로커의 자동 설정은 검증기가 하나뿐이고 그것을 감싼다. 그러므로 이 가족에서 감싸이지 않은 검증기는 이 하나다.
|
||||
|
||||
그 검증기가 무엇을 막는지는 자기 자바독이 적는다.
|
||||
|
||||
트랜잭션 식별자 접두와 멱등 생산자와 모든 복제 확인과 수동 오프셋 커밋을 요구하고, 마지막 규칙이 핵심이다. 부수효과가 데이터베이스에 사는 목적지가 브로커 트랜잭션을 함께 선언하면, 팀이 트랜잭션이라는 낱말을 두 번 읽고 전체 경로가 원자적이라고 결론짓는다는 것이다. 두 절반은 여전히 갈라질 수 있다.
|
||||
|
||||
그 검증이 지금 돌지 않는다.
|
||||
|
||||
그리고 같은 어댑터의 능력 선언은 브로커 트랜잭션을 무조건 참으로 답한다. 두 겹이 함께 비어 있다.
|
||||
|
||||
여기서 리프 경계를 넘어야만 보이는 것이 하나 있다. 검증기는 어댑터 리프가 소유하고, 그것을 시작 시 부르는 배선은 스타터가 소유한다. 어느 쪽 문서도 혼자서는 이 검증이 실행되지 않는다는 것을 말할 수 없다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
Spring Boot : 4.0.8
|
||||
확인 방식 : 자동 설정의 빈 선언 대조와 형제 자동 설정 비교
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 document-detail 의 analysis/messaging/messaging-spring-boot-starter.md 에 있다.
|
||||
|
||||
1. 시작 검증 도우미의 자바독을 읽는다.
|
||||
2. 브로커 자동 설정의 빈 선언을 순서대로 읽는다.
|
||||
3. 각 검증기에 대응하는 도우미 선언이 있는지 확인한다.
|
||||
4. 다른 브로커의 자동 설정과 비교한다.
|
||||
5. 감싸이지 않은 검증기가 무엇을 요구하는지 읽는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`KafkaProfileValidator` 는 `StartupProfileValidation` 으로 감싸여 `afterPropertiesSet` 에서 돌고, 같은 파일의 `KafkaTransactionProfileValidator` 는 빈으로 발행만 된다. Rabbit 쪽은 하나뿐인 검증기를 감싼다.
|
||||
|
||||
## KafkaProfileValidator 참조 위치
|
||||
|
||||
:::evidence key="validator-declared-and-never-injected" alt="코드베이스에서 KafkaProfileValidator 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="KafkaProfileValidator 코드베이스 검색 — 3줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 검사되지 않는 네 조건
|
||||
|
||||
트랜잭션 식별자 접두·멱등 생산자·`acks=all`·수동 커밋 요구가 시작 시 검사되지 않고, 어댑터는 `brokerTransaction=true` 를 무조건 답한다.
|
||||
|
||||
## 어느 문서도 혼자서는 이 사실을 말할 수 없다
|
||||
|
||||
검증기의 절반은 `messaging-kafka` 가, 배선의 절반은 스타터가 소유한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
조건을 어긴 프로파일로 컨텍스트를 세워 검증이 돌지 않는 것을 재현하지 않았다.
|
||||
|
||||
<!-- body:end -->
|
||||
Reference in New Issue
Block a user