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>
131 lines
6.3 KiB
Plaintext
131 lines
6.3 KiB
Plaintext
Command: python3 - <<'PY'
|
|
import json
|
|
from pathlib import Path
|
|
root=Path('src/adapter/outbound/support')
|
|
for p in sorted(x for x in root.rglob('*') if x.is_file() and 'build/' not in str(x)):
|
|
print(p)
|
|
print('--- counts ---')
|
|
for label,base,pattern in [('prod-java',root/'src/main/java','*.java'),('test-java',root/'src/test/java','*.java')]:
|
|
files=list(base.rglob(pattern)) if base.exists() else []
|
|
print(label,'files=',len(files),'loc=',sum(len(p.read_text(errors='ignore').splitlines()) for p in files))
|
|
r=json.load(open('src/config/architecture/modules.json'))['modules']
|
|
print('--- registry ---')
|
|
print(json.dumps(next(m for m in r if m['id']=='adapter-outbound-support'),indent=2))
|
|
PY
|
|
printf '%s\n' '--- build.gradle ---'; cat src/adapter/outbound/support/build.gradle
|
|
printf '%s\n' '--- README.md ---'; cat src/adapter/outbound/support/README.md
|
|
printf '%s\n' '--- CLAUDE.md ---'; cat src/adapter/outbound/support/CLAUDE.md
|
|
Working directory: /shared/codebase/clean-architecture-backend-template
|
|
Executed at: 2026-08-29T07:19:26Z
|
|
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
|
Observation boundary: Enumerates the support leaf source/build/document files and registry entry at the recorded revision; downstream consumers are not measured here.
|
|
--- stdout/stderr ---
|
|
src/adapter/outbound/support/CLAUDE.md
|
|
src/adapter/outbound/support/README.md
|
|
src/adapter/outbound/support/build.gradle
|
|
src/adapter/outbound/support/gradle.lockfile
|
|
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/package-info.java
|
|
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java
|
|
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java
|
|
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundSupportConfig.java
|
|
src/adapter/outbound/support/src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java
|
|
--- counts ---
|
|
prod-java files= 4 loc= 97
|
|
test-java files= 1 loc= 89
|
|
--- registry ---
|
|
{
|
|
"id": "adapter-outbound-support",
|
|
"gradle_path": ":adapter:outbound:support",
|
|
"source_path": "src/adapter/outbound/support",
|
|
"allowed_dependencies": [
|
|
"domain-core",
|
|
"application-core",
|
|
"shared-contract"
|
|
],
|
|
"runtime_memberships": [
|
|
"app-bootstrap"
|
|
]
|
|
}
|
|
--- build.gradle ---
|
|
// Shared base for outbound integration adapters: correlation, fail-open dependency
|
|
// logging, and the @Configuration seam. Depended on by messaging/cache/notification/httpclient.
|
|
dependencies {
|
|
implementation 'org.springframework.boot:spring-boot-autoconfigure'
|
|
implementation 'org.slf4j:slf4j-api'
|
|
}
|
|
|
|
tasks.withType(JavaCompile).configureEach {
|
|
options.encoding = 'UTF-8'
|
|
}
|
|
--- README.md ---
|
|
# adapter:outbound:support — 설계 결정 참조
|
|
|
|
아웃바운드 기술 어댑터(`:adapter:outbound:messaging`, `:adapter:outbound:cache-redis`,
|
|
`:adapter:outbound:notification`, `:adapter:outbound:httpclient`)가 공유하는 베이스 모듈. 패키지
|
|
루트: `dev.caskeleton.adapter.outbound.support`.
|
|
|
|
허용/금지 의존 정책은 `src/build.gradle` 의
|
|
`allowedProjectDependencies['adapter:outbound:support']` 항목이 SSOT 다(이 모듈은 아직 별도
|
|
CLAUDE.md 를 두지 않았다). 이 문서는 코드 주석에서 덜어낸 **설계 결정의 근거**를 모아둔 참조용
|
|
기록이다 — 코드를 읽다 "왜 이렇게 했나"가 궁금할 때 본다.
|
|
|
|
## 왜 별도 모듈인가
|
|
|
|
옛 `adapter-outbound` 모듈을 기술별(`messaging`/`cache-redis`/`notification`/`httpclient`)로
|
|
쪼개면서, 네 모듈이 공통으로 필요로 하는 조각 — correlation 조회, fail-open 의존성 로깅,
|
|
공유 `@Configuration` 등록 지점 — 을 이 모듈로 뽑아냈다. 네 기술 모듈 모두 `implementation
|
|
project(':adapter:outbound:support')` 로 이 모듈에 의존한다.
|
|
|
|
## FailOpenDependencyLogger — WARN, ERROR 아님
|
|
|
|
실패를 ERROR 가 아니라 WARN 으로 로깅한다: cache/messaging/notification 선택형 어댑터는
|
|
fail-open 이라 의존성 실패가 나도 use case 는 성공한 것이다 — 관측은 하되 escalate 하지 않는다.
|
|
이는 호출자에게 직접 노출되는 hard failure 를 ERROR 로 올리는 `httpclient` 모듈의
|
|
`OutboundHttpDependencyLogger` 와 명확히 구분된다. 메서드 시그니처가 본문·수신자·페이로드를
|
|
받지 않아 PII 가 로그에 닿지 않는다.
|
|
|
|
## OutboundCorrelation — `"unknown"` 센티넬
|
|
|
|
MDC 에 correlation id 가 없으면 `null` 이 아니라 `"unknown"` 문자열을 반환해 로그 라인에 필드가
|
|
비지 않게 한다. MDC 키는 SSOT(`mdc-keys.yaml`)를 따른다.
|
|
|
|
## OutboundSupportConfig — 공유 `@Configuration` seam
|
|
|
|
`FailOpenDependencyLogger` 빈을 `@ConditionalOnMissingBean` 으로 무조건 등록하는 단일
|
|
`@Configuration` 이다. 네 기술 모듈이 각자 로거 빈을 중복 등록하지 않고 이 모듈에만 의존하면
|
|
와이어링되며, 포크는 같은 타입의 빈을 직접 등록해 오버라이드할 수 있다. 이 모듈 자체는 어떤
|
|
`@ConditionalOnProperty` 게이팅도 갖지 않는다 — 게이팅은 각 기술 모듈이 자신의 연동 client
|
|
단위로 소유한다.
|
|
--- CLAUDE.md ---
|
|
# adapter:outbound:support — shared outbound support
|
|
|
|
## Registered identity
|
|
|
|
- Module ID: `adapter-outbound-support`
|
|
- Gradle path: `:adapter:outbound:support`
|
|
- Focused test (derived from Gradle path): `./gradlew :adapter:outbound:support:test --console=plain`
|
|
- Runtime baseline: Java 21; repository framework baseline: Spring Boot 4.0.0.
|
|
- Registry SSOT: `src/config/architecture/modules.json`.
|
|
|
|
Package roots: `dev.caskeleton.adapter.outbound` and
|
|
`dev.caskeleton.adapter.outbound.support`.
|
|
|
|
## Responsibility
|
|
|
|
- Own reusable outbound correlation, fail-open dependency logging, and auto-configuration seams.
|
|
- Provide technical support used by registered messaging/cache/notification/httpclient leaves.
|
|
- Keep feature-specific clients and routing policies in their owning leaf.
|
|
|
|
## Boundaries
|
|
|
|
- Allowed project dependencies are registry-owned: application, domain, and shared contracts.
|
|
- No inbound transport, persistence, bootstrap, or sample dependencies.
|
|
- No business rules or external-system-specific protocol implementation.
|
|
|
|
## Tests
|
|
|
|
Use focused unit tests for support behavior. Do not start a Spring application context unless the
|
|
contract being tested is configuration wiring itself.
|
|
|
|
Exit code: 0
|