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
+49
@@ -0,0 +1,49 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-inbound-graphql-c01
|
||||
title: off 계약의 두 절반과 리터럴 404를 피한 단언
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-inbound-graphql-c01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-inbound-graphql-c01
|
||||
file: ../../../final/evidence/rendered/adapter-inbound-graphql-c01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-inbound-graphql-c01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L130 이다.
|
||||
module: adapter-inbound-graphql
|
||||
---
|
||||
|
||||
# off 계약의 두 절반과 리터럴 404를 피한 단언
|
||||
|
||||
꺼짐 계약이 두 절반으로 나뉜다 — 이 leaf의 빈을 막는 절반과 프레임워크가 발행하는 라우트를 막는 절반. 뒤쪽 테스트는 리터럴 404가 아니라 매핑된 적 없는 경로의 상태 코드와 같은지를 본다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
꺼짐 계약이 두 절반으로 나뉜다.
|
||||
|
||||
| 절반 | 무엇을 막는가 | 검증 |
|
||||
|---|---|---|
|
||||
| `GraphQlRootAutoConfiguration`의 `@ConditionalOnProperty` | 이 leaf의 39개 빈 | `GraphQlShippedAndGatedTest.graphQlOffHoldsNothing` — 빈 인벤토리 |
|
||||
| `GraphQlOffAutoConfigurationImportFilter` | 프레임워크가 발행하는 `/graphql` 라우트 | `GraphQlShippedAndGatedTest.graphQlOffPublishesNoEndpoint` — 실제 포트 |
|
||||
|
||||
## 리터럴 404를 단언하지 않는 이유
|
||||
|
||||
두 번째 테스트가 특히 정교하다. 리터럴 404를 단언하지 않고, 매핑된 적 없는 경로의 상태 코드와 **같은지**를 본다 — 주석이 그 이유를 적는다: "Asserting a literal 404 would have been wrong: the security filter chain runs before ...".
|
||||
|
||||
## 그 테스트가 app-bootstrap에 있는 이유
|
||||
|
||||
그리고 그 테스트는 이 leaf가 아니라 app-bootstrap에 있다. off 계약은 출하 조립에서만 검증할 수 있으므로 옳은 위치다.
|
||||
|
||||
## 분석 원문의 두 절반 표
|
||||
|
||||
:::evidence key="adapter-inbound-graphql-c01" alt="분석 문서 analysis/16-adapter-inbound-graphql.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/16-adapter-inbound-graphql.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
<!-- body:end -->
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-inbound-web-c03
|
||||
title: security 패키지가 자기 안에서만 서로를 부른다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-inbound-web-c03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-inbound-web-c03
|
||||
file: ../../../final/evidence/rendered/adapter-inbound-web-c03.svg
|
||||
- key: adapter-inbound-web-c03-diagram
|
||||
file: ../../../final/assets/diagrams/adapter-inbound-web-c03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-inbound-web-c03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/14-adapter-inbound-web.md#L426 이다.
|
||||
module: adapter-inbound-web
|
||||
---
|
||||
|
||||
# security 패키지가 자기 안에서만 서로를 부른다
|
||||
|
||||
신원 모델 타입들의 프로덕션 참조를 전수 세면 서로를 가리키는 것뿐이고 패키지 바깥에서 들어오는 화살표가 없다. 교차 테넌트 가드 `rejectTenantInput`도 그 섬 안에만 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
신원 모델 타입들의 프로덕션 참조를 전수 세면 이렇다.
|
||||
|
||||
```text
|
||||
WebSecurityContextBridge : main_refs=0 test_refs=1
|
||||
WebActorContextResolver : main_refs=1 test_refs=0 ← 참조자는 WebSecurityContextBridge 하나
|
||||
WebTenantContextResolver : main_refs=1 test_refs=1 ← 같음
|
||||
AuthenticationView : main_refs=3 test_refs=1 ← 전부 위 세 파일
|
||||
SecurityIdentity : main_refs=1 test_refs=1
|
||||
rejectTenantInput : main_refs=2 test_refs=1 ← 선언 + 브리지 오버로드. 세 번째 호출자 없음
|
||||
WebCorsPolicyValidator : main_refs=0 test_refs=1
|
||||
WebCsrfPolicyResolver : main_refs=0 test_refs=1
|
||||
```
|
||||
|
||||
`AuthenticationView`를 만드는 코드도 테스트뿐이다 — `WebSecurityContextBridgeTest`의 다섯 줄이 전부다.
|
||||
|
||||
## 신원 모델이 닿는 범위
|
||||
|
||||
:::evidence key="adapter-inbound-web-c03-diagram" alt="security 패키지 경계 안에 세 타입이 들어 있고 바깥 프로덕션 호출자 상자가 빗금으로 경계 밖에 놓인 구조" caption="신원 모델이 닿는 범위" zoom="false"
|
||||
:::
|
||||
|
||||
즉 `security` 패키지 전체가 **자기 안에서만 서로를 부르는 닫힌 섬**이고, 바깥에서 들어오는 화살표가 없다. 교차 테넌트 가드 `rejectTenantInput`은 그 섬 안에만 있다.
|
||||
|
||||
## AuthenticationView 참조 위치
|
||||
|
||||
:::evidence key="adapter-inbound-web-c03" alt="코드베이스에서 AuthenticationView 를 검색한 출력 21줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AuthenticationView 코드베이스 검색 — 21줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
<!-- body:end -->
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-inbound-web-c04
|
||||
title: publicPaths가 먼저 등록되어 제한 경로 규칙을 덮는다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-inbound-web-c04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-inbound-web-c04
|
||||
file: ../../../final/evidence/rendered/adapter-inbound-web-c04.svg
|
||||
- key: adapter-inbound-web-c04-diagram
|
||||
file: ../../../final/assets/diagrams/adapter-inbound-web-c04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-inbound-web-c04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/14-adapter-inbound-web.md#L473 이다.
|
||||
module: adapter-inbound-web
|
||||
---
|
||||
|
||||
# publicPaths가 먼저 등록되어 제한 경로 규칙을 덮는다
|
||||
|
||||
`SecurityConfig`가 `publicPaths`를 `RestrictedPathRule`보다 먼저 등록한다. Spring Security는 첫 일치가 이기므로, 주석이 말하는 순서는 `anyRequest()`에 대해서만 성립하고 `publicPaths`에 대해서는 반대다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
매처가 등록되는 순서는 이렇다.
|
||||
|
||||
```java
|
||||
// SecurityConfig.java:83-94
|
||||
.authorizeHttpRequests(auth -> {
|
||||
if (publicPaths.length > 0) { auth.requestMatchers(publicPaths).permitAll(); } // ← 먼저
|
||||
for (RestrictedPathRule rule : restricted) { // ← 나중
|
||||
auth.requestMatchers(rule.pathPattern()).hasAnyAuthority(rule.authorities());
|
||||
}
|
||||
auth.anyRequest().authenticated();
|
||||
})
|
||||
```
|
||||
|
||||
## 매처가 등록되는 순서
|
||||
|
||||
:::evidence key="adapter-inbound-web-c04-diagram" alt="공개 경로 매처와 제한 경로 규칙과 인증 요구 매처가 왼쪽에서 오른쪽으로 이어지고 화살표에 등록 순서가 붙은 구조" caption="매처가 등록되는 순서" zoom="false"
|
||||
:::
|
||||
|
||||
Spring Security는 첫 일치가 이긴다. 주석은 "Ordered before the authenticated catch-all: **a management path must be refused at the transport**"라고 하는데, 그 순서는 `anyRequest()`에 대해서만 성립하고 `publicPaths`에 대해서는 반대다. §12.3.
|
||||
|
||||
## RestrictedPathRule 참조 위치
|
||||
|
||||
:::evidence key="adapter-inbound-web-c04" alt="코드베이스에서 RestrictedPathRule 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RestrictedPathRule 코드베이스 검색 — 8줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 실제로 등록되는 값
|
||||
|
||||
프로덕션 `RestrictedPathRule` 생산자는 하나다 — `FileserverAdminPlaneConfiguration:36`이 fileserver 관리 경로를 등록한다. `publicPaths`의 기본값은 `${SECURITY_PUBLIC_PATHS:${PRESENTATION_API_BASE_PATH:/v1}/healthcheck}`로, 환경변수 하나로 전체가 대체된다.
|
||||
|
||||
<!-- body:end -->
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-inbound-web-c07
|
||||
title: 지문이 길이 프레이밍 없이 구분자로만 만들어진다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-inbound-web-c07
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-inbound-web-c07
|
||||
file: ../../../final/evidence/rendered/adapter-inbound-web-c07.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-inbound-web-c07.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/14-adapter-inbound-web.md#L785 이다.
|
||||
module: adapter-inbound-web
|
||||
---
|
||||
|
||||
# 지문이 길이 프레이밍 없이 구분자로만 만들어진다
|
||||
|
||||
`SemanticRequestFingerprintFactory`는 U+001F 한 글자를 구분자로 쓰고 값에 이스케이프나 길이 접두사를 붙이지 않는다. 같은 저장소의 다른 다이제스트들은 4바이트 길이 프레이밍을 쓴다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`SemanticRequestFingerprintFactory`는 U+001F 한 글자를 구분자로 쓰고, 경로 변수는 `SEP + name + "=" + value`, 헤더는 `SEP + name + ":" + value`로 이어붙인다. 값에 대한 이스케이프나 길이 접두사가 없다.
|
||||
|
||||
## SemanticRequestFingerprintFactory 참조 위치
|
||||
|
||||
:::evidence key="adapter-inbound-web-c07" alt="코드베이스에서 SemanticRequestFingerprintFactory 를 검색한 출력 18줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SemanticRequestFingerprintFactory 코드베이스 검색 — 18줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 같은 저장소의 다른 다이제스트는 길이 프레이밍을 쓴다
|
||||
|
||||
notification `NotificationCatalogException.update`, messaging의 도메인 분리 상수는 **4바이트 길이 프레이밍**을 쓰고, 그 이유를 "인접 필드 연결로 인한 충돌이 구조적으로 불가능"으로 적는다. §20.3.
|
||||
|
||||
<!-- body:end -->
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-httpclient-c07
|
||||
title: 절대 URI를 정화하지 않고 거부한다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-httpclient-c07
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-httpclient-c07
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-httpclient-c07.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-httpclient-c07.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/11-adapter-outbound-httpclient.md#L474 이다.
|
||||
module: adapter-outbound-httpclient
|
||||
---
|
||||
|
||||
# 절대 URI를 정화하지 않고 거부한다
|
||||
|
||||
`TrustedTargetPolicy`는 절대 URI를 정화하는 대신 거부한다. 목적지를 바꾸는 것은 다른 계층의 일이고 그 계층은 자기 정책·자격증명·DNS 검증을 갖는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`TrustedTargetPolicy`의 규칙 — "An absolute URI is **rejected here rather than sanitised**: H2 exists to vary method, relative path, query, approved headers, and body — not the destination. Changing the destination is what H3 is for, and H3 has its own policy, credentials, and DNS validation."
|
||||
|
||||
## 템플릿이 거부되는 조건
|
||||
|
||||
`requireRelativeTemplate`가 빈 템플릿, `//` 시작, `://` 포함, `/`로 시작하지 않음을 거부한다. 확장은 문자열 연결이 아니라 Spring `DefaultUriBuilderFactory`의 `TEMPLATE_AND_VALUES` 인코딩이라 "a value containing `/`, `?`, or `#` cannot change the shape of the request." 그리고 확장 **후에** `requireAllowedOrigin`이 host/port allowlist를 다시 본다.
|
||||
|
||||
## TrustedTargetPolicy 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-httpclient-c07" alt="코드베이스에서 TrustedTargetPolicy 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TrustedTargetPolicy 코드베이스 검색 — 12줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 리다이렉트 hop에 allowlist를 다시 적용하는 이유
|
||||
|
||||
멱등성 키 처리에 수정 이력 둘이 붙어 있다. 그리고 리다이렉트 hop에 allowlist를 다시 적용하는 `requireAllowedTarget`이 public인 이유도 적혀 있다 — 조정자가 이전에는 리다이렉트 정책만 보고 프로파일 allowlist를 보지 않아 "An upstream could therefore redirect a trusted profile to any origin the redirect policy tolerated, including one the operator had explicitly excluded."
|
||||
|
||||
<!-- body:end -->
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-httpclient-c09
|
||||
title: TLS 실패가 CONNECT로 분류된 원인은 픽스처의 듀얼스택 호스트명이다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-httpclient-c09
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-httpclient-c09
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-httpclient-c09.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-httpclient-c09.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/11-adapter-outbound-httpclient.md#L627 이다.
|
||||
module: adapter-outbound-httpclient
|
||||
---
|
||||
|
||||
# TLS 실패가 CONNECT로 분류된 원인은 픽스처의 듀얼스택 호스트명이다
|
||||
|
||||
이 절은 이전 사이클이 여기에 적었던 P1 진단을 철회하고 교체한다. 관측된 실패는 그대로 재현되지만, 원인으로 지목했던 기전은 측정으로 반증되었다. 근거는 `EVD-332`다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
이 절은 이전 사이클이 여기에 적었던 **P1 진단을 철회하고 교체한다**. 관측된 실패는 그대로 재현되지만, 그 원인으로 지목했던 기전은 측정으로 반증되었다. 근거는 `EVD-332`다.
|
||||
|
||||
## 관측은 그대로다
|
||||
|
||||
`:adapter:outbound:httpclient:test` 는 HEAD 에서도 **283 중 3건 실패**한다. 세 건 모두 `MutualTlsHandshakeContractTest.java:168` — `assertThat(classified.stage()).isEqualTo(TLS_HANDSHAKE)` 다. 바로 앞줄인 167행(`evidence == NOT_SENT`)은 통과한다.
|
||||
|
||||
## 철회하는 진단
|
||||
|
||||
이전 진단은 `ApacheFailureClassifier.recognize`의 분기 순서(pool → DNS → CONNECT → TLS)를 읽고 사슬의 모양을 추론한 것이지, 사슬을 실제로 떠본 것이 아니다. 잡힌 예외를 그대로 출력하면 사슬에 `SSLHandshakeException`이 **없다**. 그림자에 가려진 것이 아니라 애초에 도착하지 않았다. 분류기는 자기가 받은 것을 정확히 분류했다.
|
||||
|
||||
## ApacheFailureClassifier 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-httpclient-c09" alt="코드베이스에서 ApacheFailureClassifier 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ApacheFailureClassifier 코드베이스 검색 — 5줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 두 주소 중 마지막 것만 호출자에게 도달한다
|
||||
|
||||
동일한 서버 객체, 동일한 클라이언트 신뢰재료. `baseUrl`의 호스트 문자열만 바꿨다. 이 컨테이너의 `/etc/hosts`는 `localhost`를 두 패밀리에 준다. `MockWebServer`는 IPv4 루프백에만 바인딩하고, `MockHttpServer.uri()`는 호스트명 `localhost`를 돌려준다 (`MockHttpServer.java:70-72`).
|
||||
|
||||
```text
|
||||
127.0.0.1 → TCP 성공 → TLS 핸드셰이크 실패(진짜 실패) → 삼켜짐
|
||||
::1 → TCP 거부(듣는 소켓 없음) → 마지막 주소 → HttpHostConnectException 으로 승격
|
||||
```
|
||||
|
||||
Apache HttpClient 5의 연결 오퍼레이터는 해석된 주소를 순회하면서 **마지막이 아닌 주소의 실패를 삼킨다**. 호출자에게 도달하는 유일한 예외는 두 번째 주소의 연결 거부다.
|
||||
|
||||
## 성공하는 테스트가 통과하는 이유도 같은 루프다
|
||||
|
||||
127.0.0.1 에서 성공하면 루프가 즉시 반환하므로 `::1`을 시도하지 않는다. 따라서 처음 눈에 띄었던 `startTls(..., true/false)` 차이는 원인이 아니라 상관관계였다 — 실패하는 케이스가 곧 두 번째 주소까지 가는 케이스다. `startTls(..., false)`에서 서버가 뜨지 않는다는 가설도 함께 기각했다. 두 경우 모두 원시 소켓 접속이 성공한다(`raw 127.0.0.1: OK`, `raw localhost: OK`).
|
||||
|
||||
<!-- body:end -->
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-notification-c04
|
||||
title: unhealthy 조건 넷에 DRAINING이 없다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-notification-c04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-notification-c04
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-notification-c04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-notification-c04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/13-adapter-outbound-notification.md#L619 이다.
|
||||
module: adapter-outbound-notification
|
||||
---
|
||||
|
||||
# unhealthy 조건 넷에 DRAINING이 없다
|
||||
|
||||
`NotificationHealthReporter.snapshot()`이 `healthy = false`로 넘어가는 조건은 넷이고, `DRAINING`은 그 목록에 없다. 로테이션 중 드레인은 정상 운영이므로 그 자체로는 옳다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`NotificationHealthReporter.snapshot()`이 `healthy = false`로 넘어가는 조건은 넷이다.
|
||||
|
||||
| 조건 | 위치 |
|
||||
|---|---|
|
||||
| 감시 대상 프로파일이 레지스트리에 없음 (`UNREGISTERED`) | `:57-62` |
|
||||
| 상태가 `AUTHENTICATION_FAILED` 또는 `DISABLED` | `:64-67` |
|
||||
| provider는 있는데 라우팅된 채널이 하나도 없음 | `:78-80` |
|
||||
| 서빙 상태가 선언된 임계치를 넘음 | `:83-85` |
|
||||
|
||||
## 세 번째 조건에 붙은 자기고발
|
||||
|
||||
"A platform with providers but no route accepts every request and delivers none. It was reported healthy because every runtime was healthy — **which was true and beside the point**."
|
||||
|
||||
## 분석 원문의 조건 표
|
||||
|
||||
:::evidence key="adapter-outbound-notification-c04" alt="분석 문서 analysis/13-adapter-outbound-notification.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/13-adapter-outbound-notification.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## DRAINING이 목록에 없다
|
||||
|
||||
여기서 눈에 띄는 것은 **`DRAINING`이 목록에 없다**는 점이다. 로테이션 중 드레인은 정상 운영이므로 그 자체로는 옳다. 그러나 §13의 P2와 겹치면 부작용이 하나 더 생긴다 — 아래 §21.2.
|
||||
|
||||
<!-- body:end -->
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-objectstorage-c03
|
||||
title: ObjectReference가 보장하는 route 구획
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-objectstorage-c03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-objectstorage-c03
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-objectstorage-c03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-objectstorage-c03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/09-adapter-outbound-objectstorage.md#L135 이다.
|
||||
module: adapter-outbound-objectstorage
|
||||
---
|
||||
|
||||
# ObjectReference가 보장하는 route 구획
|
||||
|
||||
`RoutingObjectReadAdapter.load`는 `canonicalText`를 분리한 뒤 두 번째 구획을 직접 읽는다. 별도 인덱스 검사는 없지만 `ObjectReference` 생성자가 `ObjectIdentitySupport.requireRouted(canonicalText, "osr1")`로 형식을 강제하므로, 유효한 참조에는 route 구획이 항상 존재한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
## 직접 인덱싱과 입력 불변식
|
||||
|
||||
`RoutingObjectReadAdapter.load`는 `reference.canonicalText().split("\\.", -1)[1]`로 route token을 읽는다. 이 코드만 보면 두 번째 구획이 없을 때 `ArrayIndexOutOfBoundsException`이 발생할 수 있다.
|
||||
|
||||
## RoutingObjectReadAdapter 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-objectstorage-c03" alt="코드베이스에서 RoutingObjectReadAdapter 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RoutingObjectReadAdapter 코드베이스 검색 — 5줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## ObjectReference가 보장하는 형식
|
||||
|
||||
`ObjectReference` 생성자는 `ObjectIdentitySupport.requireRouted(canonicalText, "osr1")`를 통해 routed 형식을 먼저 검증한다. 따라서 정상적으로 생성된 `ObjectReference`가 `load`에 전달되는 경로에서는 두 번째 구획이 값 타입의 불변식으로 보장된다. 현재 계약에서는 이 split 자체를 결함으로 분류할 근거가 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c53
|
||||
title: 가드의 존재가 곧 테넌트 격리 보장은 아니다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c53
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c53
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c53.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c53.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L3784 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# 가드의 존재가 곧 테넌트 격리 보장은 아니다
|
||||
|
||||
`TenantAwareRepositoryGuard`와 `TenantEntityListenerGuard`의 local behavior는 fail-closed다. 그러나 현재 production repository/entity에 연결된 caller/listener registration이 없다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`TenantAwareRepositoryGuard`와 `TenantEntityListenerGuard`의 local behavior는 fail-closed다.
|
||||
|
||||
## TenantAwareRepositoryGuard 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c53" alt="코드베이스에서 TenantAwareRepositoryGuard 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TenantAwareRepositoryGuard 코드베이스 검색 — 3줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 존재만으로 격리를 주장하지 않는 이유
|
||||
|
||||
현재 production repository/entity에 연결된 caller/listener registration은 없다. 따라서 이 type들이 존재한다는 이유만으로 현재 application의 tenant isolation이 보장된다고 쓰지 않는다.
|
||||
|
||||
<!-- body:end -->
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-support-c03
|
||||
title: 로그 마스킹은 PII 차단 보장을 복구하지 않는다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-support-c03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-support-c03
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-support-c03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-support-c03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/04-adapter-outbound-support.md#L207 이다.
|
||||
module: adapter-outbound-support
|
||||
---
|
||||
|
||||
# 로그 마스킹은 PII 차단 보장을 복구하지 않는다
|
||||
|
||||
`app-bootstrap`의 `LogMaskingPatterns`는 방어 심층화로 몇 가지 secret 형태를 mask하지만 arbitrary email address나 free-form body PII를 일반적으로 제거하는 규칙은 없다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`app-bootstrap`의 `LogMaskingPatterns`는 방어 심층화로 다음과 같은 secret 형태를 mask한다.
|
||||
|
||||
- password/secret/token/api-key 계열 key=value
|
||||
- Authorization credentials
|
||||
- standalone Bearer token
|
||||
|
||||
## LogMaskingPatterns 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-support-c03" alt="코드베이스에서 LogMaskingPatterns 를 검색한 출력 29줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="LogMaskingPatterns 코드베이스 검색 — 29줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 마스킹 규칙이 덮지 않는 것
|
||||
|
||||
arbitrary email address나 free-form body PII를 일반적으로 제거하는 규칙은 없다. app-bootstrap README 자체도 regex masking을 **보증이 아니라 defence-in-depth**라고 설명한다. 따라서 현재 "logger signature 때문에 PII가 들어올 수 없다"는 1차 방어선 설명은 사실과 맞지 않는다.
|
||||
|
||||
<!-- body:end -->
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-support-c05
|
||||
title: outbound 어댑터끼리는 support를 통해서만 공유한다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-support-c05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-support-c05
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-support-c05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-support-c05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/04-adapter-outbound-support.md#L368 이다.
|
||||
module: adapter-outbound-support
|
||||
---
|
||||
|
||||
# outbound 어댑터끼리는 support를 통해서만 공유한다
|
||||
|
||||
`CleanArchitectureTest.OUTBOUND_ADAPTERS_ARE_PEERS_SHARING_ONLY_SUPPORT`가 outbound adapter family를 slice로 나누고 서로 직접 의존하지 못하게 한다. 유일한 shared-code 예외는 target package가 `..adapter.outbound.support..`인 dependency다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`CleanArchitectureTest.OUTBOUND_ADAPTERS_ARE_PEERS_SHARING_ONLY_SUPPORT`는 outbound adapter family를 slice로 나누고 서로 직접 의존하지 못하게 한다. 유일한 shared-code 예외는 target package가 이것인 dependency다.
|
||||
|
||||
```text
|
||||
..adapter.outbound.support..
|
||||
```
|
||||
|
||||
## 규칙이 허용하는 것과 금지하는 것
|
||||
|
||||
따라서 messaging → notification 같은 peer coupling은 금지하지만 messaging → support는 허용한다. fresh `CleanArchitectureTest --rerun-tasks`도 통과했다.
|
||||
|
||||
## 분석 원문의 규칙 서술
|
||||
|
||||
:::evidence key="adapter-outbound-support-c05" alt="분석 문서 analysis/04-adapter-outbound-support.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/04-adapter-outbound-support.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 이 규칙이 고정하는 것
|
||||
|
||||
support 모듈이 단순 편의 library가 아니라 **outbound family에서 sanctioned shared dependency point**라는 점을 build-time fitness function으로 고정한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: app-bootstrap-c05
|
||||
title: 레지스트리 계약 테스트가 실제 파일을 읽어 대조한다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:app-bootstrap-c05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: app-bootstrap-c05
|
||||
file: ../../../final/evidence/rendered/app-bootstrap-c05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/app-bootstrap-c05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/18-app-bootstrap.md#L460 이다.
|
||||
module: app-bootstrap
|
||||
---
|
||||
|
||||
# 레지스트리 계약 테스트가 실제 파일을 읽어 대조한다
|
||||
|
||||
`MasterSwitchRegistryContractTest`가 `docs/registries/env-keys.yaml`과 `src/app-bootstrap/src/main/resources/application.yml`을 실제로 읽어 대조한다. 파일 기반 SSOT가 테스트로 고정돼 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`MasterSwitchRegistryContractTest`가 `docs/registries/env-keys.yaml`과 `src/app-bootstrap/src/main/resources/application.yml`을 실제로 읽어 대조한다(§2). `ErrorCodeRegistryMappingTest`·`SecretsClassificationRegistryTest`도 `docs/registries/` 아래 파일을 읽는다.
|
||||
|
||||
## MasterSwitchRegistryContractTest 참조 위치
|
||||
|
||||
:::evidence key="app-bootstrap-c05" alt="코드베이스에서 MasterSwitchRegistryContractTest 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MasterSwitchRegistryContractTest 코드베이스 검색 — 3줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 파일 기반 SSOT가 테스트로 고정돼 있다
|
||||
|
||||
레지스트리 파일이 문서로만 존재하지 않고, 그 내용과 실제 설정 파일이 어긋나면 테스트가 먼저 깨진다.
|
||||
|
||||
<!-- body:end -->
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: application-core-c02
|
||||
title: 권한 판정과 객체 접근 판정을 분리한다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:application-core-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: application-core-c02
|
||||
file: ../../../final/evidence/rendered/application-core-c02.svg
|
||||
- key: application-core-c02-diagram
|
||||
file: ../../../final/assets/diagrams/application-core-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/application-core-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/03-application-core.md#L78 이다.
|
||||
module: application-core
|
||||
---
|
||||
|
||||
# 권한 판정과 객체 접근 판정을 분리한다
|
||||
|
||||
`AuthorizationPort`는 "이 종류의 작업을 수행할 수 있는가"를 판정하고, object-level access는 별도 `ObjectAccessPolicy`가 담당한다. 같은 permission을 가진 사용자라도 ownership, membership, workflow state에 따라 결과가 달라질 수 있기 때문이다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **legacy storage/notification compatibility surface의 제거 조건 추적**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`AuthorizationPort`는 principal의 raw role/permission을 기준으로 "이 종류의 작업을 수행할 수 있는가"를 판정하는 framework-free PEP다. `AuthorizationPrincipal`은 role set을 defensive copy + unmodifiable로 만들고 null roles는 empty set으로 정규화한다. `AuthorizationDeniedException`은 Spring `AccessDeniedException` 대신 application-owned failure를 사용한다.
|
||||
|
||||
## 두 판정이 갈리는 자리
|
||||
|
||||
:::evidence key="application-core-c02-diagram" alt="요청에서 권한 포트와 객체 접근 정책으로 각각 화살표가 나가고 화살표에 작업 종류와 개별 객체가 붙은 구조" caption="두 판정이 갈리는 자리" zoom="false"
|
||||
:::
|
||||
|
||||
object-level access는 별도 `ObjectAccessPolicy`가 담당한다. 같은 permission을 가진 사용자라도 ownership, membership, workflow state에 따라 특정 object 접근 결과가 달라질 수 있기 때문이다. `ObjectAccessDecision`은 denial에 stable code를 요구하고 `hideExistence`를 별도 boolean으로 보존해 transport가 403/404 disclosure 정책을 추측하지 않게 한다.
|
||||
|
||||
## AuthorizationPort 참조 위치
|
||||
|
||||
:::evidence key="application-core-c02" alt="코드베이스에서 AuthorizationPort 를 검색한 출력 28줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AuthorizationPort 코드베이스 검색 — 28줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 이 계약이 adapter에서 core로 옮겨 온 이력
|
||||
|
||||
`ObjectAccessPolicyTest`에는 이 계약이 과거 inbound GraphQL adapter에 있었고 GraphQL request context를 signature에 포함해 application-core가 구현하려면 transport에 역의존해야 했던 문제가 기록돼 있다. 현재 regression test는 policy/request/decision signature에 `dev.caskeleton.adapter.*` 타입이 다시 등장하면 실패한다. 이 프로젝트에서 "여러 호출자가 공유해야 하는 계약을 inbound adapter가 소유하면 Core가 Adapter에 의존하게 된다"는 문제가 실제로 있었던 근거다.
|
||||
|
||||
## 계약에 명시된 한계
|
||||
|
||||
`decideAll()`의 default는 요청 순서를 보존하지만 object마다 `decide()`를 호출한다. set-based authorization을 제공하는 구현체가 override하지 않으면 batched loading 안에서 authorization N+1을 다시 만들 수 있다는 제한도 계약에 명시돼 있다.
|
||||
|
||||
<!-- body:end -->
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: grpc-advanced-bootstrap-c03
|
||||
title: 승격 게이트는 능력마다 따로 기록된 증거를 본다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:grpc-advanced-bootstrap-c03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-bootstrap-c03
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-bootstrap-c03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-bootstrap-c03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-bootstrap.md#L89 이다.
|
||||
module: grpc-advanced-bootstrap
|
||||
---
|
||||
|
||||
# 승격 게이트는 능력마다 따로 기록된 증거를 본다
|
||||
|
||||
증거를 능력마다 따로 기록하는 이유가 적혀 있다 — 공유 기록은 하나를 승격할 때 같은 시점에 측정된 다른 것들까지 함께 승격시킨다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
증거는 능력마다 따로 기록된다.
|
||||
|
||||
> "a shared record makes promoting one of them promote whichever others happened to be measured at the same time."
|
||||
|
||||
일곱 항목(호환성·보안 검토·고장·성능·ADR·런북·실환경 테스트)과 담금 기간을 본다.
|
||||
|
||||
## 담금 기간이 둘인 이유
|
||||
|
||||
```text
|
||||
ADVANCED_STABLE_SOAK = 7일
|
||||
STABLE_DEFAULT_SOAK = 30일
|
||||
```
|
||||
|
||||
> "becoming a Stable default means every deployment gets it, which additionally puts its dependencies on every classpath and its failure modes in every on-call rotation."
|
||||
|
||||
## GrpcAdvancedSupportMatrix 참조 위치
|
||||
|
||||
:::evidence key="grpc-advanced-bootstrap-c03" alt="코드베이스에서 GrpcAdvancedSupportMatrix 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcAdvancedSupportMatrix 코드베이스 검색 — 6줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 시작 등급을 다시 확인하는 이유
|
||||
|
||||
그리고 `WATCH` 는 `EXPERIMENTAL` 을 먼저 거쳐야 한다. `GrpcAdvancedSupportMatrix.apply` 는 결정의 시작 등급이 현재 등급과 다르면 거부한다 — 두 승격이 경합했거나 하나가 재생된 경우다.
|
||||
|
||||
<!-- body:end -->
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: grpc-advanced-diagnostics-c01
|
||||
title: Channelz는 유용해서 위험한 표면이다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:grpc-advanced-diagnostics-c01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-diagnostics-c01
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-diagnostics-c01.svg
|
||||
- key: grpc-advanced-diagnostics-c01-diagram
|
||||
file: ../../../final/assets/diagrams/grpc-advanced-diagnostics-c01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-diagnostics-c01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-diagnostics.md#L41 이다.
|
||||
module: grpc-advanced-diagnostics
|
||||
---
|
||||
|
||||
# Channelz는 유용해서 위험한 표면이다
|
||||
|
||||
이 리프는 진단 표면(Channelz·CSDS)과 그것을 게시 가능하게 만드는 편집기, 그리고 고급 능력의 검증 대상을 이름 짓는 테스트킷 계약을 담는다. 편집기가 필요한 이유가 편집기 자신의 javadoc에 적혀 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
진단 표면(Channelz·CSDS)과 그것을 게시 가능하게 만드는 편집기, 그리고 고급 능력이 무엇을 상대로 검증되어야 하는지를 이름 짓는 테스트킷 계약을 담는다.
|
||||
|
||||
## 이 리프가 담는 것
|
||||
|
||||
:::evidence key="grpc-advanced-diagnostics-c01-diagram" alt="리프 경계 안에 진단 표면과 편집기와 테스트킷 계약 세 상자가 나란히 들어 있는 구조" caption="이 리프가 담는 것" zoom="false"
|
||||
:::
|
||||
|
||||
## 편집기가 필요한 이유
|
||||
|
||||
편집기 javadoc 이 왜 이것이 필요한지 적는다.
|
||||
|
||||
> "Channelz is unusually dangerous to expose because it is genuinely useful: it holds every socket's local and remote address, the security details of each connection, and per-call state… once the endpoint exists the whole of it is one authorization mistake away from being readable."
|
||||
|
||||
## 이 기록이 다루는 파일 범위
|
||||
|
||||
:::evidence key="grpc-advanced-diagnostics-c01" alt="코드베이스에서 파일 목록을 만든 출력 4줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 4줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
<!-- body:end -->
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: grpc-observability-c01
|
||||
title: 논리 RPC와 물리 시도와 스트림 수명주기를 구별한다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:grpc-observability-c01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-observability-c01
|
||||
file: ../../../final/evidence/rendered/grpc-observability-c01.svg
|
||||
- key: grpc-observability-c01-diagram
|
||||
file: ../../../final/assets/diagrams/grpc-observability-c01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-observability-c01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-observability.md#L46 이다.
|
||||
module: grpc-observability
|
||||
---
|
||||
|
||||
# 논리 RPC와 물리 시도와 스트림 수명주기를 구별한다
|
||||
|
||||
관측을 세 층위로 나눈다. Micrometer를 `api`로 노출하는 이유도 build.gradle에 적혀 있고 코드와 일치한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
build.gradle 주석이 이 리프의 범위를 적는다.
|
||||
|
||||
```groovy
|
||||
// build.gradle:3-5
|
||||
// Bounded observability: logical RPC vs physical attempt vs stream lifecycle, with a cardinality
|
||||
// policy that refuses payload, raw metadata and any actor/tenant/object/stream/idempotency
|
||||
// identifier as a tag.
|
||||
```
|
||||
|
||||
## 구별하는 세 층위
|
||||
|
||||
:::evidence key="grpc-observability-c01-diagram" alt="리프 경계 안에 논리 RPC 와 물리 시도와 스트림 수명주기 세 상자가 나란히 들어 있는 구조" caption="구별하는 세 층위" zoom="false"
|
||||
:::
|
||||
|
||||
## Micrometer를 api로 노출하는 이유
|
||||
|
||||
build.gradle 에 적혀 있다 — "the observation convention's public signatures name Micrometer types, so wiring it requires naming them." 실제로 `GrpcObservationConvention` 의 생성자와 `boundedTags` 반환형이 Micrometer 타입(`MeterRegistry`, `Tags`)이므로 그 서술은 코드와 일치한다.
|
||||
|
||||
## GrpcObservationConvention 참조 위치
|
||||
|
||||
:::evidence key="grpc-observability-c01" alt="코드베이스에서 GrpcObservationConvention 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcObservationConvention 코드베이스 검색 — 12줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
<!-- body:end -->
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: grpc-spring-boot-starter-c01
|
||||
title: Advanced 의존 금지를 세 겹으로 강제한다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:grpc-spring-boot-starter-c01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-spring-boot-starter-c01
|
||||
file: ../../../final/evidence/rendered/grpc-spring-boot-starter-c01.svg
|
||||
- key: grpc-spring-boot-starter-c01-diagram
|
||||
file: ../../../final/assets/diagrams/grpc-spring-boot-starter-c01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-spring-boot-starter-c01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-spring-boot-starter.md#L42 이다.
|
||||
module: grpc-spring-boot-starter
|
||||
---
|
||||
|
||||
# Advanced 의존 금지를 세 겹으로 강제한다
|
||||
|
||||
이 리프가 `:grpc-advanced:*`에 닿으면 안 된다는 규칙이 주석이 아니라 세 지점에서 각각 강제된다 — 레지스트리, 빌드 검증 태스크, 자바 쪽 단언.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
build.gradle 주석이 이 리프의 경계를 적는다.
|
||||
|
||||
```groovy
|
||||
// build.gradle:3-7
|
||||
// The platform's composition boundary: typed properties, auto-configuration and the startup
|
||||
// validator that refuses a deployment whose configuration contradicts a Stable invariant.
|
||||
//
|
||||
// It must never reach `:grpc-advanced:*`. That is not a comment — the registry's
|
||||
// allowed_dependencies for this leaf omits every advanced id, `verifyCleanArchitectureDependencies`
|
||||
// enforces it, and GrpcPlatformStartupValidatorTest asserts the same rule from the Java side.
|
||||
```
|
||||
|
||||
## 한 규칙을 지키는 세 지점
|
||||
|
||||
:::evidence key="grpc-spring-boot-starter-c01-diagram" alt="Advanced 의존 금지 규칙에서 레지스트리와 빌드 검증 태스크와 자바 단언 세 상자로 화살표가 나가는 구조" caption="한 규칙을 지키는 세 지점" zoom="false"
|
||||
:::
|
||||
|
||||
격리 규칙은 세 겹이다 — 레지스트리, 빌드 검증 태스크, 그리고 자바 쪽 단언. 세 번째는 `validateAdvancedIsolation` 이 `GrpcStableBuildInvariant.requireNoAdvancedDependency` 를 부르는 형태다.
|
||||
|
||||
## GrpcStableBuildInvariant 참조 위치
|
||||
|
||||
:::evidence key="grpc-spring-boot-starter-c01" alt="코드베이스에서 GrpcStableBuildInvariant 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcStableBuildInvariant 코드베이스 검색 — 10줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
<!-- body:end -->
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-admin-api-c02
|
||||
title: 부팅된 애플리케이션에서 살아나는 타입은 둘뿐이다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-admin-api-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-api-c02
|
||||
file: ../../../final/evidence/rendered/messaging-admin-api-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-api-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-api.md#L98 이다.
|
||||
module: messaging-admin-api
|
||||
---
|
||||
|
||||
# 부팅된 애플리케이션에서 살아나는 타입은 둘뿐이다
|
||||
|
||||
leaf 밖 소비자는 21개 파일 4개 모듈이지만, 부팅된 애플리케이션에서 실제로 살아나는 것은 둘뿐이다. `MessagingAdminService` 빈도 `ApprovalVerifier` 빈도 없다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`messaging-core-api` 에서 쓰는 것: `DestinationName`, `MessageAuthorizationException`, `MessagingConfigurationException`. `messaging-policy` 는 `api` 로 선언돼 있지만 import 0건이다(§12.4).
|
||||
|
||||
## leaf 밖 소비자 21개 파일
|
||||
|
||||
| 모듈 | src/main | src/test | 역할 |
|
||||
|---|---:|---:|---|
|
||||
| `messaging-admin-runtime` | 10 | 6 | 실제 실행·검증 서비스 |
|
||||
| `messaging-spring-boot-starter` | 2 | 0 | 빈 배선 + 저널 내구성 검사 |
|
||||
| `messaging-outbox-jdbc-postgresql` | 1 | 1 | `JdbcAdminOperationJournal` |
|
||||
| `messaging-kafka` | 0 | 1 | `KafkaTopologyValidationIT` |
|
||||
|
||||
## DestinationName 참조 위치
|
||||
|
||||
:::evidence key="messaging-admin-api-c02" alt="코드베이스에서 DestinationName 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DestinationName 코드베이스 검색 — 16줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 빈이 없는 것이 설계인 범위
|
||||
|
||||
부팅된 애플리케이션에서 이 리프의 타입 중 실제로 살아나는 것은 **둘뿐**이다(`EVD-302`, `EVD-303`). `MessagingAdminService` 빈은 없고 `ApprovalVerifier` 빈도 없다. 이는 명시된 설계다. 그러나 이 스탠스가 **토폴로지 검증까지 덮지는 않는다** — §12.1 과 §17 의 첫 항목이 그것이다.
|
||||
|
||||
<!-- body:end -->
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-admin-api-c05
|
||||
title: 인가 실패와 프로그래밍 오류를 예외 타입으로 가른다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-admin-api-c05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-api-c05
|
||||
file: ../../../final/evidence/rendered/messaging-admin-api-c05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-api-c05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-api.md#L543 이다.
|
||||
module: messaging-admin-api
|
||||
---
|
||||
|
||||
# 인가 실패와 프로그래밍 오류를 예외 타입으로 가른다
|
||||
|
||||
거절은 전부 `MessageAuthorizationException` 또는 `MessagingConfigurationException`이고 안정 코드가 붙는다. `IllegalArgumentException`은 구조적으로 불가능한 값에만 쓴다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
거절은 전부 `MessageAuthorizationException` 또는 `MessagingConfigurationException` 이고, 코드가 붙어 있다. 메시지가 전부 "무엇이 왜 거절되었는가" 를 서술형으로 쓴다.
|
||||
|
||||
| 코드 | 던지는 곳 | 의미 |
|
||||
|---|---|---|
|
||||
| `APPROVAL_PLAN_MISMATCH` | `HmacApprovalVerifier:74`, `ApprovedReplayPlan:26`, `ApprovedRedrivePlan:28` | 진짜 승인, 다른 계획 |
|
||||
| `APPROVAL_EXPIRED` | `HmacApprovalVerifier:81`, `DestructiveOperationGuard:69` | 윈도우 밖 |
|
||||
| `APPROVAL_SIGNATURE_INVALID` | `HmacApprovalVerifier:89, :93` | 16진 아님 / HMAC 불일치 |
|
||||
| `ADMIN_CREDENTIAL_REQUIRED` | `DestructiveOperationGuard:58` | 애플리케이션 런타임 |
|
||||
| `APPROVAL_REQUIRED` | `DestructiveOperationGuard:65` | 승인 없음 |
|
||||
| `APPROVAL_OPERATION_MISMATCH` | `DestructiveOperationGuard:75` | 다른 작업의 승인 |
|
||||
| `APPROVAL_SOURCE_MISMATCH` | `DestructiveOperationGuard:85` | 다른 목적지의 승인 |
|
||||
| `APPROVAL_IMPACT_EXCEEDED` | `Approved*Plan:47/52` | 승인 상한 초과 |
|
||||
| `TOPOLOGY_CHANGED_SINCE_APPROVAL` | `Approved*Plan:70/74, :79/82` | 추정치 무효 |
|
||||
| `REDRIVE_LOOP_NOT_ACKNOWLEDGED` | `ApprovedRedrivePlan:88` | 루프 위험 미승인 |
|
||||
| `AUTO_CREATE_IN_PRODUCTION` | `TopologyManagementMode:30` | 프로덕션 자동 생성 |
|
||||
| `TOPOLOGY_MISMATCH` | `TopologyValidationReport:78` | BLOCKING 존재 |
|
||||
|
||||
## MessageAuthorizationException 참조 위치
|
||||
|
||||
:::evidence key="messaging-admin-api-c05" alt="코드베이스에서 MessageAuthorizationException 를 검색한 출력 38줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageAuthorizationException 코드베이스 검색 — 38줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## IllegalArgumentException을 쓰는 자리
|
||||
|
||||
`IllegalArgumentException` 은 **구조적으로 불가능한 값**에만 쓴다 — 음수 카운터, 빈 문자열, 역전된 윈도우, 4-eyes 위반. 인가 실패와 프로그래밍 오류가 예외 타입으로 갈린다.
|
||||
|
||||
## 로그 위생이 예외마다 같지 않다
|
||||
|
||||
`toString()` 하나가 로그 위생을 명시적으로 다룬다. `ApprovalGrant` 는 record 라 기본 `toString()` 이 전 필드를 찍는다는 점은 대비된다 — 다만 `ApprovalGrant` 자체가 로그에 닿는 경로는 확인되지 않았다.
|
||||
|
||||
<!-- body:end -->
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-rabbit-c02
|
||||
title: 자격증명은 연결 시도마다 해석된다
|
||||
topic: security-and-trust-boundaries
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-rabbit-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-rabbit-c02
|
||||
file: ../../../final/evidence/rendered/messaging-rabbit-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-rabbit-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-rabbit.md#L106 이다.
|
||||
module: messaging-rabbit
|
||||
---
|
||||
|
||||
# 자격증명은 연결 시도마다 해석된다
|
||||
|
||||
RabbitMQ 클라이언트 연결은 오래 살고 스스로 재연결하므로, 시작 시점의 자격증명을 들고 있는 팩토리는 폐기된 자격증명으로 계속 재연결한다. 재연결 시점이 곧 회전된 자격증명이 적용되어야 할 시점이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
자격증명을 연결 시도마다 다시 해석하는 이유가 적혀 있다.
|
||||
|
||||
> "RabbitMQ client connections are long-lived and reconnect on their own, so a factory holding a credential from startup will happily reconnect with a revoked one for as long as the process runs — the reconnect is exactly the moment a rotated credential should take effect."
|
||||
|
||||
## AmqpCredentials가 record가 아닌 이유
|
||||
|
||||
비밀을 지우려면 가변이어야 하고, record 가 `char[]` 를 동등성에 쓰면 같은 자재를 가진 둘이 서로 다르다고 판정된다.
|
||||
|
||||
## AmqpCredentials 참조 위치
|
||||
|
||||
:::evidence key="messaging-rabbit-c02" alt="코드베이스에서 AmqpCredentials 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AmqpCredentials 코드베이스 검색 — 9줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
<!-- body:end -->
|
||||
Reference in New Issue
Block a user