Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/158-cache-redis-module-inventory.txt
T
DongHyeonkaandClaude Opus 5 b2963105a8 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>
2026-09-04 22:51:59 +09:00

338 lines
21 KiB
Plaintext

adapter-outbound-cache-redis — 모듈 인벤토리
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
generatedAt=2026-08-30T03:57:11+00:00
=== tracked file 총계 ===
$ git ls-files 'adapter/outbound/cache-redis' | wc -l
390
exit=0
$ git ls-files 'adapter/outbound/cache-redis' | sed 's|^adapter/outbound/cache-redis/||' | awk -F/ '{print $1"/"$2}' | sort | uniq -c | sort -rn
316 src/main
70 src/test
1 gradle.lockfile/
1 build.gradle/
1 README.md/
1 CLAUDE.md/
exit=0
=== source set 별 ===
main: 316 files, 33489 LOC
test: 70 files, 15244 LOC
=== main 패키지 배치 ===
$ git ls-files 'adapter/outbound/cache-redis/src/main' | grep '\.java$' | sed 's|.*/dev/caskeleton/||' | xargs -n1 dirname | sort | uniq -c | sort -rn
69 adapter/outbound/cache/redis/sdk/api/operations
67 adapter/outbound/cache/redis/sdk/lettuce/operations
17 adapter/outbound/cache/redis/sdk/programmability
17 adapter/outbound/cache/redis/sdk/api/reactive
17 adapter/outbound/cache/redis/sdk/api/key
14 adapter/outbound/cache/redis/sdk/lettuce/command
14 adapter/outbound/cache/redis/sdk/api/error
13 adapter/outbound/cache/redis/sdk/api/command
12 adapter/outbound/cache/redis/sdk/config
9 adapter/outbound/cache/redis/sdk/lettuce/connection
7 adapter/outbound/cache/redis/sdk/raw
7 adapter/outbound/cache/redis/sdk/lettuce/codec
7 adapter/outbound/cache/redis/sdk/extensions/search
6 adapter/outbound/cache/redis/sdk/api
5 adapter/outbound/cache/redis/sdk/cluster
4 adapter/outbound/cache/redis/sdk/extensions/timeseries
4 adapter/outbound/cache/redis/sdk/admin
4 adapter/outbound/cache/redis/realtime
3 adapter/outbound/cache/redis/sdk/extensions/json
3 adapter/outbound/cache/redis/sdk/api/codec
3 adapter/outbound/cache/redis/ratelimit
2 adapter/outbound/cache/redis/sdk/lettuce/observability
2 adapter/outbound/cache/redis/sdk/extensions/probabilistic
2 adapter/outbound/cache/redis/lease
2 adapter/outbound/cache/redis/idempotency
2 adapter/outbound/cache/redis/cache
1 adapter/outbound/cache/redis/sdk/extensions
1 adapter/outbound/cache/redis/keyspace
exit=0
=== main 상위 30 파일 ===
$ git ls-files 'adapter/outbound/cache-redis/src/main' | grep '\.java$' | xargs wc -l | sort -rn | head -31
32082 total
1282 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/LettuceRedisCommandGateway.java
1227 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/RedisCommandGateway.java
959 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkSettings.java
600 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/RedisTopologyClientFactory.java
529 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/idempotency/RedisIdempotencyStoreAdapter.java
487 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/StreamOperationRequests.java
428 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/cache/RedisCacheRegionAdapter.java
422 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/HashOperationRequests.java
413 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/SortedSetOperationRequests.java
405 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/RedisOperationContext.java
403 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/lease/RedisDistributedLeaseAdapter.java
397 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkAutoConfiguration.java
361 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/programmability/LettuceRedisTransactionOperations.java
344 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/ListOperationRequests.java
337 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/admin/LettuceRedisAdminOperations.java
333 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/SetOperationRequests.java
320 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/ValueOperationRequests.java
316 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/RedisRuntimeOwner.java
306 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/KeyOperationRequests.java
306 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/idempotency/IdempotencyScripts.java
301 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/realtime/RedisConnectionRegistryAdapter.java
280 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/ratelimit/RateLimitScripts.java
278 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/CommandPolicyGuard.java
268 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/BatchExecution.java
268 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/RedisCommandPolicyLoader.java
251 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/GeoOperationRequests.java
247 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/LettuceRedisBatch.java
234 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslator.java
230 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/extensions/timeseries/LettuceRedisTimeSeriesOperations.java
220 adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/extensions/probabilistic/LettuceRedisProbabilisticOperations.java
exit=0
=== governance / 루트 파일 ===
$ git ls-files 'adapter/outbound/cache-redis' | grep -v '^adapter/outbound/cache-redis/src/'
adapter/outbound/cache-redis/CLAUDE.md
adapter/outbound/cache-redis/README.md
adapter/outbound/cache-redis/build.gradle
adapter/outbound/cache-redis/gradle.lockfile
exit=0
=== 레지스트리 엔트리 ===
$ python3 -c "
import json;d=json.load(open('config/architecture/modules.json'))
for m in d['modules']:
if m['id']=='adapter-outbound-cache-redis': print(json.dumps(m,ensure_ascii=False,indent=2))
"
{
"id": "adapter-outbound-cache-redis",
"gradle_path": ":adapter:outbound:cache-redis",
"source_path": "src/adapter/outbound/cache-redis",
"allowed_dependencies": [
"domain-core",
"application-core",
"shared-contract",
"adapter-outbound-support"
],
"runtime_memberships": [
"app-bootstrap"
]
}
exit=0
=== build.gradle ===
$ cat -n 'adapter/outbound/cache-redis/build.gradle'
1 // Redis SDK leaf — see docs/superpowers/specs/2026-08-07-redis-wrapper-typed-api-design.md.
2 //
3 // The design models the SDK as separate Gradle modules. This repository's fail-closed module
4 // registry outranks that layout, so the module boundaries are packages under
5 // dev.caskeleton.adapter.outbound.cache.redis.sdk and RedisSdkModuleBoundaryTest enforces them.
6 dependencies {
7 // Registered edges the semantic port adapters need. The SDK itself imports nothing from them
8 // today (0 imports across main source) — the semantic cache/session/idempotency/rate-limit
9 // adapters that did were removed and are restored by Phase E of
10 // docs/superpowers/plans/2026-08-10-redis-optionality-and-composition.md. They stay declared
11 // because that restoration is the module's stated responsibility, not because anything here
12 // compiles against them.
13 implementation project(':application-core')
14 implementation project(':shared-contract')
15 implementation project(':adapter:outbound:support')
16
17 implementation 'org.springframework.boot:spring-boot-autoconfigure'
18 // The role-aware health contributors are HealthIndicators; the readiness probe is the only
19 // place the required/optional Redis taxonomy can actually be enforced.
20 implementation 'org.springframework.boot:spring-boot-health'
21 // Boot's Health type carries Jackson annotations. Without the annotations on the compile
22 // classpath javac emits an 'unknown enum constant' warning, and this build is -Werror. Runtime
23 // does not need it from here — the app already has Jackson — so compileOnly is the honest scope.
24 compileOnly 'com.fasterxml.jackson.core:jackson-annotations'
25 implementation 'io.lettuce:lettuce-core'
26 // Reactor is in the public signature of sdk.api.reactive, and it was reaching this module only
27 // transitively through lettuce-core. A driver upgrade that stopped exposing it would have
28 // broken compilation of the SDK's own published API, so it is declared directly.
29 implementation 'io.projectreactor:reactor-core'
30 implementation 'org.slf4j:slf4j-api'
31 annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
32
33 // Deliberately absent:
34 // org.springframework.data:spring-data-redis — the SDK owns its own typed API and command
35 // policy on purpose; routing through Spring Data would reintroduce the untyped, unguarded
36 // command surface the catalog exists to prevent. Zero imports.
37 // io.micrometer:micrometer-core — observation leaves this leaf as RedisObservation through a
38 // Consumer sink; binding it to a meter registry belongs to the composition root, not here.
39 // Zero imports.
40 }
41
42 tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' }
43
44 // The topology lane is opt-in and fail-closed. The default unit task excludes it, and selecting it
45 // without an endpoint is an error rather than a skip: a topology test that silently passes because
46 // it never connected is worse than not having one.
47 tasks.named('test') {
48 useJUnitPlatform {
49 excludeTags 'redis-topology'
50 }
51 }
52
53 // Lane selection is derived from the declared mode rather than chosen by hand. A promotion test is
54 // meaningless without sentinels and a cross-slot test is meaningless without a cluster, but writing
55 // that as a runtime assumption would turn "the lane was never started" into a green skip. Selecting
56 // by tag keeps the lane fail-closed: what a mode cannot prove is not selected, and what is selected
57 // must pass.
58 //
59 // The mode is an allowlist, not free text. Deriving the tag from an arbitrary property produced the
60 // worst possible result for a qualification lane: `-Predis.topology.mode=TYPO` built the tag
61 // `lane-typo`, matched nothing, ran zero tests and exited 0. A release gate that reports success
62 // for a lane it never ran is worse than no gate, so an unknown mode is an error and a run that
63 // executed no test is a failure.
64 // `tls` is a lane, not a deployment mode. Its shape is standalone; what it qualifies is the
65 // transport, which no other lane carries a single command over. It was reachable only by hand —
66 // point LiveRedisCompositionTest at the TLS compose with an ad-hoc init script — which is another
67 // way of saying the release gate did not cover TLS at all.
68 def REDIS_TOPOLOGY_MODES = ['standalone', 'sentinel', 'cluster', 'tls'] as Set
69 def REDIS_TOPOLOGY_DEPLOYMENT_MODE = ['standalone': 'standalone', 'sentinel': 'sentinel',
70 'cluster': 'cluster', 'tls': 'standalone']
71 // The classes each lane exists to run, and the floor below which its coverage has shrunk. Both are
72 // declarations rather than observations: a lane that lost a class to a rename, or lost half its
73 // cases to a filter, otherwise still reports success.
74 def REDIS_TOPOLOGY_REQUIRED_CLASSES = [
75 'standalone': ['LiveRedisCompositionTest', 'LiveRedisSemanticPortsTest',
76 'RedisTopologyContractTest', 'LiveRedisGuardrailTest'],
77 'sentinel' : ['LiveRedisCompositionTest', 'LiveRedisSentinelPromotionTest',
78 'RedisTopologyContractTest'],
79 'cluster' : ['LiveRedisCompositionTest', 'LiveRedisClusterTest',
80 'LiveRedisClusterTransactionTest', 'LiveRedisSemanticPortsTest'],
81 'tls' : ['LiveRedisTlsTest'],
82 ]
83 def REDIS_TOPOLOGY_MINIMUM_TESTS = ['standalone': 20, 'sentinel': 20, 'cluster': 24, 'tls': 4]
84
85 tasks.register('redisTopologyTest', Test) {
86 description = 'Runs the Redis SDK contracts against a real topology declared in infra/redis-sdk.'
87 group = 'verification'
88 testClassesDirs = sourceSets.test.output.classesDirs
89 classpath = sourceSets.test.runtimeClasspath
90 // Never up to date. This task's result depends on a server outside the build, so Gradle's
91 // inputs say nothing about whether it would still pass: re-running it against a lane that was
92 // restarted, reconfigured, or promoted reports the previous run's verdict as the current one.
93 // That is the same silent-pass failure mode the fail-closed endpoint check exists to prevent.
94 outputs.upToDateWhen { false }
95 def declaredMode = (project.findProperty('redis.topology.mode') ?: 'unset').toString().toLowerCase()
96 useJUnitPlatform {
97 includeTags "redis-topology & lane-${declaredMode}".toString()
98 }
99 // A filter that matches nothing is a configuration mistake, never a pass.
100 failOnNoDiscoveredTests = true
101 ['redis.topology.host', 'redis.topology.port',
102 'redis.topology.master', 'redis.topology.username', 'redis.topology.password',
103 'redis.topology.trust-material']
104 .each { key ->
105 if (project.hasProperty(key)) {
106 systemProperty key, project.property(key)
107 }
108 }
109 // The lane name and the deployment mode are different things, and only the TLS lane makes that
110 // visible: its shape is standalone, so the tests must see `standalone` while the tag filter and
111 // the required properties come from the lane. Passing the lane name through as the mode would
112 // fail RedisDeploymentMode.valueOf on a value that is not a topology.
113 systemProperty 'redis.topology.mode', REDIS_TOPOLOGY_DEPLOYMENT_MODE.getOrDefault(declaredMode, declaredMode)
114 systemProperty 'redis.topology.tls', (declaredMode == 'tls').toString()
115
116 // Executed, not merely reported. `afterTest` fires for a skipped test too, so counting every
117 // callback meant a lane whose tests all skipped could still satisfy the "ran something" check —
118 // the exact green-for-nothing this gate exists to prevent, one level further in.
119 def executed = new java.util.concurrent.atomic.AtomicInteger()
120 def skipped = new java.util.concurrent.atomic.AtomicInteger()
121 def classes = java.util.Collections.synchronizedSet(new java.util.LinkedHashSet<String>())
122 afterTest { descriptor, result ->
123 if (result.resultType == org.gradle.api.tasks.testing.TestResult.ResultType.SKIPPED) {
124 skipped.incrementAndGet()
125 } else {
126 executed.incrementAndGet()
127 classes.add(descriptor.className.tokenize('.').last())
128 }
129 }
130
131 doFirst {
132 if (!REDIS_TOPOLOGY_MODES.contains(declaredMode)) {
133 throw new GradleException(
134 "redisTopologyTest was selected with redis.topology.mode='${declaredMode}'; " +
135 'the supported modes are ' + REDIS_TOPOLOGY_MODES.sort().join(', ') +
136 '. An unrecognised mode selects no test and would otherwise report success.')
137 }
138 def required = ['redis.topology.host', 'redis.topology.port']
139 if (declaredMode == 'sentinel') {
140 required += 'redis.topology.master'
141 }
142 if (declaredMode == 'tls') {
143 // Without the trust material the client would have to disable verification to connect,
144 // and a TLS lane that trusts anything qualifies nothing.
145 required += 'redis.topology.trust-material'
146 }
147 def missing = required.findAll { !project.hasProperty(it) }
148 if (!missing.isEmpty()) {
149 throw new GradleException(
150 'redisTopologyTest was selected without ' + missing.join(', ') +
151 '; start a lane from infra/redis-sdk and pass -P<key>=<value>.')
152 }
153 // The lane's tag must actually exist in the compiled suite. failOnNoDiscoveredTests catches
154 // an empty run, but this names the cause — a renamed or deleted lane class — instead of
155 // leaving an operator to guess whether the filter or the server is at fault.
156 def laneTag = "lane-${declaredMode}"
157 def tagged = sourceSets.test.allJava.matching { include '**/*.java' }.files.any { file ->
158 def text = file.text
159 text.contains('@Tag("redis-topology")') && text.contains("@Tag(\"${laneTag}\")")
160 }
161 if (!tagged) {
162 throw new GradleException(
163 "redisTopologyTest found no test class tagged 'redis-topology' and " +
164 "'${laneTag}'. The ${declaredMode} lane has no coverage to run, so a green " +
165 'result would prove nothing.')
166 }
167 }
168
169 doLast {
170 if (executed.get() < 1) {
171 throw new GradleException(
172 "redisTopologyTest completed without executing a single test for the " +
173 "${declaredMode} lane. A qualification lane that runs nothing must not report " +
174 'success.')
175 }
176 // What a lane must cover, named rather than counted by accident. A tag filter matching one
177 // trivial class satisfied "ran something" while the class the lane exists for had been
178 // renamed out of the filter, and nothing said so.
179 def required = REDIS_TOPOLOGY_REQUIRED_CLASSES[declaredMode]
180 def absent = required.findAll { !classes.contains(it) }
181 if (!absent.isEmpty()) {
182 throw new GradleException(
183 "redisTopologyTest ran the ${declaredMode} lane without ${absent.join(', ')}. " +
184 'These classes are what the lane qualifies; a run that skipped them proves ' +
185 'less than the lane claims.')
186 }
187 def floor = REDIS_TOPOLOGY_MINIMUM_TESTS[declaredMode]
188 if (executed.get() < floor) {
189 throw new GradleException(
190 "redisTopologyTest executed ${executed.get()} tests for the ${declaredMode} " +
191 "lane, below the declared floor of ${floor}. Coverage that silently shrank is " +
192 'a gate that silently weakened.')
193 }
194 if (skipped.get() > 0) {
195 throw new GradleException(
196 "redisTopologyTest skipped ${skipped.get()} test(s) on the ${declaredMode} " +
197 'lane. A qualification lane has no conditional coverage: what it cannot prove ' +
198 'must not be selected, and what is selected must run.')
199 }
200 logger.lifecycle("redisTopologyTest: ${declaredMode} lane executed ${executed.get()} tests.")
201 }
202 }
exit=0
=== leaf 밖 소비자 ===
$ git grep -l 'dev.caskeleton.adapter.outbound.cache' -- . | grep -v '^adapter/outbound/cache-redis/' | sed 's|/src/.*||' | sort | uniq -c
7 app-bootstrap
exit=0
=== test resources ===
$ git ls-files 'adapter/outbound/cache-redis/src' | grep -v '\.java$' | grep -v '\.groovy$'
adapter/outbound/cache-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
adapter/outbound/cache-redis/src/main/resources/redis-sdk/redis-command-policy.yml
adapter/outbound/cache-redis/src/test/resources/redis-sdk/golden/order-summary-v1.json
exit=0