adapter-outbound-httpclient — 모듈 인벤토리 revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5 generatedAt=2026-08-30T05:19:48+00:00 === tracked file 총계 === $ git ls-files 'adapter/outbound/httpclient' | wc -l 370 exit=0 $ for d in $(git ls-files 'adapter/outbound/httpclient/src' | sed 's|^adapter/outbound/httpclient/src/||' | cut -d/ -f1 | sort -u); do n=$(git ls-files "adapter/outbound/httpclient/src/$d" | wc -l); l=$(git ls-files "adapter/outbound/httpclient/src/$d" | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}'); echo "$d: $n files, $l LOC"; done httpClientPerformanceTest: 7 files, 495 LOC jmh: 2 files, 130 LOC main: 260 files, 15004 LOC test: 62 files, 6049 LOC testkit: 35 files, 2754 LOC exit=0 === main 패키지 배치 === $ git ls-files 'adapter/outbound/httpclient/src/main' | grep '\.java$' | sed 's|.*/dev/caskeleton/||' | xargs -n1 dirname | sort | uniq -c | sort -rn 39 adapter/outbound/httpclient/resilience 29 adapter/outbound/httpclient/profile 23 adapter/outbound/httpclient/api/error 21 adapter/outbound/httpclient/security 19 adapter/outbound/httpclient/restclient 18 adapter/outbound/httpclient/auth 17 adapter/outbound/httpclient/webclient 14 adapter/outbound/httpclient/service 11 adapter/outbound/httpclient/dynamic 10 adapter/outbound/httpclient/transport 9 adapter/outbound/httpclient/api/result 7 adapter/outbound/httpclient/api/body 6 adapter/outbound/httpclient/reactor 6 adapter/outbound/httpclient/observation 6 adapter/outbound/httpclient/api/operation 5 adapter/outbound/httpclient/api 5 adapter/outbound/httpclient/apache 4 adapter/outbound/httpclient/migration 4 adapter/outbound/httpclient/jdk 4 adapter/outbound/httpclient/http3 3 adapter/outbound/httpclient/spring7 exit=0 === test 패키지 배치 === $ git ls-files 'adapter/outbound/httpclient/src/test' | grep '\.java$' | sed 's|.*/dev/caskeleton/||' | xargs -n1 dirname | sort | uniq -c | sort -rn 8 adapter/outbound/httpclient/resilience 7 adapter/outbound/httpclient/contract 5 adapter/outbound/httpclient/webclient 5 adapter/outbound/httpclient/service 5 adapter/outbound/httpclient/security 5 adapter/outbound/httpclient/restclient 3 adapter/outbound/httpclient/architecture 2 adapter/outbound/httpclient/testkit 2 adapter/outbound/httpclient/profile 2 adapter/outbound/httpclient/observation 2 adapter/outbound/httpclient/migration 2 adapter/outbound/httpclient/jdk 2 adapter/outbound/httpclient/dynamic 2 adapter/outbound/httpclient/auth 2 adapter/outbound/httpclient/api/body 2 adapter/outbound/httpclient/apache 1 adapter/outbound/httpclient/transport 1 adapter/outbound/httpclient/spring7 1 adapter/outbound/httpclient/http3 1 adapter/outbound/httpclient/api/operation 1 adapter/outbound/httpclient/api/error 1 adapter/outbound/httpclient/api exit=0 === main 상위 30 === $ git ls-files 'adapter/outbound/httpclient/src/main' | grep '\.java$' | xargs wc -l | sort -rn | head -31 15004 total 330 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/webclient/DefaultReactiveHttpGateway.java 304 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/restclient/DefaultGenericHttpGateway.java 291 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/profile/ClientProfileValidator.java 272 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/service/ReactiveKernelHttpExchangeAdapter.java 269 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/restclient/BlockingAttemptExecutor.java 228 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/dynamic/DefaultDynamicTargetGateway.java 215 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/dynamic/IpAddressClassifier.java 211 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/service/KernelHttpExchangeAdapter.java 206 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/restclient/RestClientResponseReader.java 190 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/http3/JettyHttp3TransportProvider.java 187 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/profile/ClientRuntimeRegistry.java 184 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/webclient/ReactiveAttemptExecutor.java 183 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/webclient/DefaultReactiveSseGateway.java 176 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/security/TrustedTargetPolicy.java 176 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/security/TlsMaterialProvider.java 169 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/restclient/BlockingStreamingGateway.java 165 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/api/error/HttpFailureMetadata.java 154 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/service/ServiceOperationDescriptorScanner.java 153 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/apache/ApacheClientFactory.java 152 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/api/operation/HttpOperation.java 148 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/auth/SingleFlightTokenLoader.java 144 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/restclient/RestClientRuntimeFactory.java 140 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/apache/ApacheFailureClassifier.java 137 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/reactor/ReactorHttpClientFactory.java 137 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/reactor/ReactorFailureClassifier.java 135 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/resilience/AttemptResiliencePipeline.java 133 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/resilience/DefaultRetryEligibilityEngine.java 132 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/restclient/BlockingClientRuntime.java 128 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/auth/OAuth2CredentialProvider.java 123 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/webclient/WebClientRuntimeFactory.java exit=0 === 비 Java 파일 === $ git ls-files 'adapter/outbound/httpclient' | grep -v '\.java$' adapter/outbound/httpclient/CLAUDE.md adapter/outbound/httpclient/README.md adapter/outbound/httpclient/build.gradle adapter/outbound/httpclient/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-httpclient': print(json.dumps(m,ensure_ascii=False,indent=2)) " { "id": "adapter-outbound-httpclient", "gradle_path": ":adapter:outbound:httpclient", "source_path": "src/adapter/outbound/httpclient", "allowed_dependencies": [ "domain-core", "application-core", "shared-contract", "adapter-outbound-support" ], "runtime_memberships": [ "app-bootstrap" ] } exit=0 === build.gradle === $ cat -n 'adapter/outbound/httpclient/build.gradle' 1 // Outbound HTTP Client Platform leaf — see 2 // docs/superpowers/specs/2026-08-08-httpclient-platform-design.md (design package) and 3 // docs/httpclient/repository-adaptation.md (how the design's 19 library modules map here). 4 // 5 // The design models the platform as 19 separate Gradle modules. This repository's fail-closed 6 // module registry (src/config/architecture/modules.json) outranks that layout, so the module 7 // boundaries are packages under dev.caskeleton.adapter.outbound.httpclient and 8 // HttpClientModuleBoundaryTest enforces the design's module dependency table. 9 description = 'Outbound adapter: HTTP client platform (typed clients, profiles, evidence-based retry)' 10 11 dependencies { 12 implementation project(':application-core') 13 implementation project(':shared-contract') 14 implementation project(':adapter:outbound:support') 15 16 // Spring client layer. `httpclient-core-api` must not reach these; ArchUnit enforces it. 17 implementation 'org.springframework.boot:spring-boot-autoconfigure' 18 implementation 'org.springframework:spring-web' 19 implementation 'org.springframework:spring-webflux' 20 implementation 'io.projectreactor:reactor-core' 21 22 // Transport providers. Apache HC5 is the blocking default, JDK HttpClient is the lightweight 23 // alternative (JDK built-in), Reactor Netty is the reactive default, Jetty carries the 24 // Experimental HTTP/3 transport that the Stable starter never auto-configures. 25 implementation 'org.apache.httpcomponents.client5:httpclient5' 26 implementation 'io.projectreactor.netty:reactor-netty-http' 27 implementation 'org.eclipse.jetty:jetty-client' 28 // HTTP/3 is Experimental and off by default, so its transport is compileOnly plus a test 29 // dependency rather than a runtime one. It used to be `implementation`, which put the whole 30 // QUIC/HTTP-3/QPACK stack on every deployment's runtime classpath — megabytes and an attack 31 // surface — to serve a feature the Stable starter never auto-configures. A deployment that 32 // opts into HTTP/3 adds `org.eclipse.jetty.http3:jetty-http3-client-transport` itself, and 33 // Http3CapabilityReport already refuses the transport when those classes are absent, so the 34 // failure mode is a startup error rather than a NoClassDefFoundError mid-call. 35 compileOnly 'org.eclipse.jetty.http3:jetty-http3-client-transport' 36 testImplementation 'org.eclipse.jetty.http3:jetty-http3-client-transport' 37 38 // Resilience4j supplies the execution primitives only. HTTP retry *eligibility* is owned by 39 // this module (design D-09) and never delegated to a generic retry library. 40 implementation 'io.github.resilience4j:resilience4j-retry:2.2.0' 41 implementation 'io.github.resilience4j:resilience4j-circuitbreaker:2.2.0' 42 implementation 'io.github.resilience4j:resilience4j-ratelimiter:2.2.0' 43 implementation 'io.github.resilience4j:resilience4j-bulkhead:2.2.0' 44 implementation 'io.github.resilience4j:resilience4j-micrometer:2.2.0' 45 46 implementation 'org.springframework.security:spring-security-oauth2-client' 47 implementation 'com.fasterxml.jackson.core:jackson-databind' 48 // Jackson 3, and not optional. `RestClientResponseReader.defaultConverters()` constructs a 49 // `JacksonJsonHttpMessageConverter`, which is Spring 7's converter and is built on 50 // `tools.jackson.databind.json.JsonMapper`. javac never noticed: the constructor's signature 51 // does not mention the type, so compiling the call needs nothing. Loading the class does, and 52 // Jackson 3 reached only this leaf's test and jmh classpaths — so the reader worked in every 53 // test and would have thrown NoClassDefFoundError in any deployment that did not happen to 54 // have Jackson 3 from somewhere else. The Eclipse compiler the editor runs reported it as an 55 // error, which is what it is. Jackson 2 stays: two files still read `JsonNode` with it. 56 implementation 'tools.jackson.core:jackson-databind' 57 implementation 'io.micrometer:micrometer-core' 58 implementation 'org.slf4j:slf4j-api' 59 60 61 // Testkit dependencies (design §28.1 test topology). They are test-scoped so no production 62 // module can depend on the testkit; the testkit source set inherits them by extending 63 // testImplementation, and the test lanes use them directly. 64 testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0' 65 testImplementation 'com.squareup.okhttp3:okhttp-tls:4.12.0' 66 testImplementation 'org.testcontainers:testcontainers' 67 testImplementation 'org.testcontainers:testcontainers-junit-jupiter' 68 testImplementation 'org.testcontainers:testcontainers-toxiproxy' 69 testImplementation 'io.projectreactor:reactor-test' 70 testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0' 71 testImplementation 'io.projectreactor.tools:blockhound:1.0.17.RELEASE' 72 } 73 74 // The testkit is its own source set, not part of `test`, because three lanes consume it and only 75 // one of them is a test lane. Reaching into `sourceSets.test.output` from `jmh` compiled under 76 // Gradle and could not be modelled by the IDE at all: a source set is test source there only when 77 // a Test task runs its output, `jmh` is driven by JavaExec, and IDE main source may not read IDE 78 // test source — so every testkit reference in the benchmarks was an unresolved type in the editor 79 // while the build was green. A source set nobody runs tests from is main source for all three 80 // consumers, which is what it always was. 81 // 82 // Performance certification and JMH benchmarks are separate source sets for their own reason: they 83 // are slow, they assert on resource bounds rather than behaviour, and they must never be part of 84 // the default unit lane. 85 strictTestLanes { 86 // The testkit compiles against exactly what a test does: `implementation` inheritance runs 87 // through testImplementation, so this is the module's own dependencies plus the test libraries. 88 sourceSet('testkit') { compilesAgainst 'main' } 89 sourceSet('httpClientPerformanceTest') { compilesAgainst 'main', 'testkit' } 90 sourceSet('jmh') { compilesAgainst 'main', 'testkit' } 91 } 92 93 // Every test lane compiles and runs against the testkit. 94 sourceSets.test { 95 compileClasspath += sourceSets.testkit.output 96 runtimeClasspath += sourceSets.testkit.output 97 } 98 99 dependencies { 100 // Testkit dependencies (design §28.1 test topology). They stay off the production 101 // configurations, so no production module can depend on the testkit. 102 testkitImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0' 103 testkitImplementation 'com.squareup.okhttp3:okhttp-tls:4.12.0' 104 testkitImplementation 'org.testcontainers:testcontainers' 105 testkitImplementation 'org.testcontainers:testcontainers-junit-jupiter' 106 testkitImplementation 'org.testcontainers:testcontainers-toxiproxy' 107 testkitImplementation 'io.projectreactor:reactor-test' 108 testkitImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0' 109 testkitImplementation 'io.projectreactor.tools:blockhound:1.0.17.RELEASE' 110 111 jmhImplementation 'org.openjdk.jmh:jmh-core:1.37' 112 jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' 113 } 114 115 // UTF-8 is pinned for every JavaCompile task in the root build; this leaf no longer repeats it. 116 117 // JMH generates its harness classes at compile time. They are not our source, so the 118 // compile-time checker and -Werror are switched off for that source set only; applying them 119 // would fail the build on generated code we cannot edit. 120 tasks.named('compileJmhJava', JavaCompile) { 121 options.errorprone.enabled = false 122 options.compilerArgs.removeIf { it == '-Werror' } 123 } 124 125 // The bytecode analyser is disabled for the same generated harness, for the same reason. 126 tasks.named('spotbugsJmh') { 127 enabled = false 128 } 129 130 // Takes a Test task. The parameter is left untyped because the IDE's Gradle parser has no Gradle 131 // API on its classpath and reports the annotation as an unresolved type; Groovy dispatches the 132 // calls below dynamically either way. 133 Closure applyContractSelection = { task -> 134 // Cross-transport contract lane. The same semantic contract runs against every Stable transport; 135 // the transport under test is selected explicitly so a missing transport is an error, not a skip. 136 task.systemProperty 'httpclient.contract.transports', 137 (project.findProperty('httpclient.contract.transports') ?: 'apache,jdk,reactor').toString() 138 // Netty's strictest leak detector is on for every lane. It is only meaningful if it is actually 139 // live, so NettyLeakDetectionExtension asserts the level rather than trusting the flag reached 140 // the forked JVM. 141 task.systemProperty 'io.netty.leakDetection.level', 'paranoid' 142 // HTTP/3 is Experimental: it is never part of the default lane and never silently skipped. 143 task.systemProperty 'httpclient.http3.tests.enabled', 144 (project.findProperty('http3.tests.enabled') ?: 'false').toString() 145 } 146 147 tasks.named('test', Test) { 148 applyContractSelection(it) 149 // Two lanes are excluded from the default run for opposite reasons: the fault lane needs Docker 150 // and fails closed without it, and the BlockHound lane rewrites core JDK bytecode, which must 151 // not be imposed on every unit run. 152 useJUnitPlatform { 153 excludeTags 'quarantine', 'httpclient-fault', 'httpclient-blockhound' 154 } 155 } 156 157 // Four tag-selected lanes, declared rather than assembled. 158 // 159 // Two of them — the stable contract suite and the security suite — did not carry 160 // failOnNoDiscoveredTests at all. Five lanes were written by copying the block above, and the 161 // property that makes a lane mean anything was lost on two of the copies, so the cross-transport 162 // contract suite and the SSRF/credential-leak suite would each have reported success on discovering 163 // nothing. Declaring the lanes removes the opportunity: the convention has no opt-out. 164 strictTestLanes { 165 lane('httpClientBlockHoundTest') { 166 tag = 'httpclient-blockhound' 167 description = 'Proves no platform code blocks a Reactor event loop (design §18.2, §28.6).' 168 customize = { test -> 169 applyContractSelection(test) 170 // BlockHound instruments already-loaded JDK classes; Java 13+ needs this to redefine them. 171 test.jvmArgs '-XX:+AllowRedefinitionToAddDeleteMethods' 172 } 173 } 174 lane('httpClientStableContractTest') { 175 tag = 'httpclient-contract' 176 description = 'Runs the cross-transport stable contract suite (design §28.2, §33).' 177 customize = { test -> applyContractSelection(test) } 178 } 179 lane('httpClientSecurityTest') { 180 tag = 'httpclient-security' 181 description = 'Runs the SSRF, credential-leak, and cardinality suite (design §28.5, §28.7).' 182 customize = { test -> applyContractSelection(test) } 183 } 184 // Its own source set rather than a tag, so the source set is the selection. 185 lane('httpClientPerformanceTest') { 186 sourceSet = 'httpClientPerformanceTest' 187 description = 'Certifies pool, streaming, retry, and rotation resource bounds (design §28.8).' 188 customize = { test -> 189 applyContractSelection(test) 190 test.systemProperty 'performance.assertions.enabled', 191 (project.findProperty('performance.assertions.enabled') ?: 'false').toString() 192 } 193 } 194 lane('httpClientFailureInjectionTest') { 195 tag = 'httpclient-fault' 196 description = 'Runs the Toxiproxy fault-injection suite; fails closed without Docker ' + 197 '(design §28.3).' 198 customize = { test -> 199 applyContractSelection(test) 200 // The upstream image is mutable by default. Passing a digest here is what makes a red 201 // fault run attributable to this repository rather than to someone else's image push. 202 test.systemProperty 'httpclient.fault.httpbin.image', 203 (project.findProperty('httpclient.fault.httpbin.image') 204 ?: 'kennethreitz/httpbin:latest').toString() 205 } 206 } 207 } 208 209 210 tasks.register('jmh', JavaExec) { 211 group = 'verification' 212 description = 'Runs the JMH benchmarks for the blocking and reactive clients (design §28.8).' 213 mainClass = 'org.openjdk.jmh.Main' 214 classpath = sourceSets.jmh.runtimeClasspath 215 args '-rf', 'json', '-rff', layout.buildDirectory.file('reports/jmh/result.json').get().asFile.absolutePath 216 } 217 218 // Spring 6.2 / 7.0 compatibility lanes. This repository's Spring Boot 4.0 baseline pins Spring 219 // Framework 7, so the 6.2 lane verifies the *API surface* the common packages compile against 220 // rather than executing on a 6.2 distribution; the limitation is recorded in 221 // docs/httpclient/support-matrix.md instead of being hidden behind a green check. 222 tasks.register('spring62ApiSurfaceScan', Test) { 223 group = 'verification' 224 description = 'Scans the common packages for Spring 6.2 API-surface confinement. NOT a 6.2 runtime.' 225 testClassesDirs = sourceSets.test.output.classesDirs 226 classpath = sourceSets.test.runtimeClasspath 227 useJUnitPlatform { includeTags 'httpclient-spring62-surface' } 228 failOnNoDiscoveredTests = true 229 outputs.upToDateWhen { false } 230 } 231 232 // Which lanes gate an ordinary build, and which do not. 233 // 234 // The specialised lanes existed but hung off nothing: `check` ran only `test`, so the SSRF suite, 235 // the BlockHound lane, the cross-transport contract and the Spring 6.2 surface scan were green in 236 // CI only because a workflow happened to name them, and green locally because nobody ran them. 237 // The four below are hermetic and fast — no Docker, no network, no machine-dependent thresholds — 238 // so they belong in `check`. 239 // 240 // httpClientFailureInjectionTest (needs Docker), httpClientPerformanceTest (machine-dependent 241 // bounds) and jmh (minutes) stay out deliberately. Attaching them would make `check` fail on a 242 // laptop without Docker, which teaches people to skip `check`. 243 tasks.named('check') { 244 dependsOn 'httpClientStableContractTest', 245 'httpClientSecurityTest', 246 'httpClientBlockHoundTest', 247 'spring62ApiSurfaceScan' 248 } 249 250 tasks.register('spring70CompatibilityTest', Test) { 251 group = 'verification' 252 description = 'Runs the contract suite on the repository Spring 7 baseline (design §29).' 253 testClassesDirs = sourceSets.test.output.classesDirs 254 classpath = sourceSets.test.runtimeClasspath 255 useJUnitPlatform { includeTags 'httpclient-contract' } 256 applyContractSelection(it) 257 outputs.upToDateWhen { false } 258 } exit=0 === leaf 밖 소비자 === $ git grep -l 'dev.caskeleton.adapter.outbound.httpclient' -- . | grep -v '^adapter/outbound/httpclient/' | sed 's|/src/.*||' | sort | uniq -c 17 app-bootstrap exit=0