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()) 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=.') 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