Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/a14-f014-advanced.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

173 lines
8.7 KiB
Plaintext

# 열거형이 세운 규칙과 상수
/**
* The web capabilities that are not part of Stable, each behind its own flag.
*
* <p>One flag per capability, not one for "advanced". They have nothing in common operationally:
* virtual threads change how every request is scheduled, streaming changes how long a response
* holds a connection, XML adds a parser with a decades-long history of entity-expansion attacks. A
* single switch would make those one decision, and a deployment that wanted the first would be
* given the third.
*
* <p>Every constant is off unless named.
*/
선언된 상수 수 : 12
24: MVC_VIRTUAL_THREADS,
27: WEBFLUX_BLOCKING_BRIDGE,
30: JSON_MERGE_PATCH,
33: JSON_PATCH,
36: SSE,
39: NDJSON,
42: JSON_SEQUENCE,
45: FUNCTIONAL_WEBFLUX,
48: CBOR,
51: XML,
54: OPENAPI_32,
57: RATELIMIT_DRAFT_HEADERS;
각 상수가 발행하는 속성 이름 :
/** The property that turns this on. */
public String propertyName() {
return "backend.web.advanced." + name().toLowerCase(Locale.ROOT).replace('_', '-') + ".enabled";
}
# main 에 그 접두사가 나오는 자리 전부. 만드는 쪽과 읽는 쪽이 섞여 있다
VirtualThreadProfile.java:76 return "backend.web.advanced.virtual-threads.enabled";
MvcStreamingExecutorConfiguration.java:37 prefix = "backend.web.advanced.ndjson",
VirtualThreadSettings.java:10 * Settings bound from {@code backend.web.advanced.mvc-virtual-threads.*}.
VirtualThreadSettings.java:17 @ConfigurationProperties(prefix = "backend.web.advanced.mvc-virtual-threads")
VirtualThreadMvcConfiguration.java:33 prefix = "backend.web.advanced.mvc-virtual-threads",
WebAdvancedFeature.java:61 return "backend.web.advanced." + name().toLowerCase(Locale.ROOT).replace('_', '-') + ".enabled";
그중 게이트나 바인딩인 셋 :
17:@ConfigurationProperties(prefix = "backend.web.advanced.mvc-virtual-threads")
33: prefix = "backend.web.advanced.mvc-virtual-threads",
34- name = "enabled",
35- havingValue = "true")
36-public class VirtualThreadMvcConfiguration {
37: prefix = "backend.web.advanced.ndjson",
38- name = "enabled",
39- havingValue = "true")
40-public class MvcStreamingExecutorConfiguration {
이름을 만들기만 하는 둘 :
public String propertyName() {
return "backend.web.advanced." + name().toLowerCase(Locale.ROOT).replace('_', '-') + ".enabled";
/** The property that turns this on. */
public static String propertyName() {
return "backend.web.advanced.virtual-threads.enabled";
}
# 두 스위치를 켜면 각각 무엇이 생기는가
* <p>Named {@code applicationTaskExecutor} because that is the bean Spring Boot's MVC async
* support looks for. A differently named bean is created, is never used, and leaves the container
* default in place — which is the failure mode where the whole capability is switched on and
* nothing changes.
*/
@Bean("applicationTaskExecutor")
@ConditionalOnMissingBean(name = "applicationTaskExecutor")
그런데 그 이름을 출하 조립 경로가 이미 쥐고 있다 :
16: * with a bounded pool; naming the bean {@value #EXECUTOR_BEAN_NAME} backs off Boot's auto-config
19:@Configuration
25: public static final String EXECUTOR_BEAN_NAME = "applicationTaskExecutor";
40: @Bean(name = EXECUTOR_BEAN_NAME)
41: @Primary
42: ThreadPoolTaskExecutor applicationTaskExecutor(
52: executor.setRejectedExecutionHandler(new LoggingAbortPolicy(EXECUTOR_BEAN_NAME, metrics));
57: EXECUTOR_BEAN_NAME, () -> executor.getThreadPoolExecutor().getQueue().size());
컴포넌트 스캔 제외 정규식에 bootstrap.async 가 있는가 : 0
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(
prefix = "backend.web.advanced.ndjson",
name = "enabled",
havingValue = "true")
public class MvcStreamingExecutorConfiguration {
72: return new MvcStreamWriter(encoder, StreamFraming.NDJSON);
84: return new MvcStreamWriter(encoder, StreamFraming.JSON_SEQUENCE);
그 두 미디어 타입을 내는 main 라우트 : 0
MvcStreamWriter 를 advanced 밖에서 부르는 main 코드 : 0
# 레인이 무엇을 고르고 그 안에서 무엇을 단언하는가
tasks.register('webAdvancedTest', Test) {
group = 'verification'
description = 'Runs every web Advanced capability contract.'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform {
includeTags 'web-advanced'
}
failOnNoDiscoveredTests = true
그 태그를 단 시험 클래스 수 : 22
그 파일에서 속성을 참으로 두는 자리. 셋은 만들어지는 빈을, 둘은 기동 실패를 단언한다 :
43: "backend.web.advanced.mvc-virtual-threads.enabled=true",
72: .withPropertyValues("backend.web.advanced.ndjson.enabled=true")
92: "backend.web.advanced.mvc-virtual-threads.enabled=true",
117: "backend.web.advanced.mvc-virtual-threads.enabled=true",
136: .withPropertyValues("backend.web.advanced.mvc-virtual-threads.enabled=true")
그중 켠 뒤 빈을 확인하는 자리 :
.withPropertyValues("backend.web.advanced.ndjson.enabled=true")
.withUserConfiguration(MvcStreamingExecutorConfiguration.class)
.run(
context ->
assertThat(context)
.hasNotFailed()
.hasSingleBean(WebStreamPolicy.class)
.hasSingleBean(WebStreamRegistry.class)
.hasSingleBean(StreamRecordEncoder.class)
.hasBean("mvcNdjsonWriter")
.hasBean("mvcJsonSequenceWriter"));
# 롤백 시험 일곱이 무엇을 단언하는가
49: void nothingIsCreatedWithNoFlags() {
67: void explicitFalseCreatesNothing() {
86: void flagsReportStablePreservation() {
103: void codecsOffLeaveOnlyJson() {
119: void draftHeadersOffWriteNothing() {
134: void functionalRoutingOffServesNothing() {
146: void everyGateDemandsRollbackEvidence() {
속성 상태를 다루는 둘 — 하나는 값을 두지 않고, 하나는 거짓으로 둔다 :
void nothingIsCreatedWithNoFlags() {
// No property values at all — the shape a Stable deployment actually has.
runner
.withUserConfiguration(
.withPropertyValues(
"backend.web.advanced.ndjson.enabled=false",
"backend.web.advanced.mvc-virtual-threads.enabled=false")
.withUserConfiguration(
플래그 값을 쓰는 유일한 시험 전문 :
void flagsReportStablePreservation() {
assertThat(
dev.caskeleton.adapter.inbound.web.advanced.WebAdvancedFeatureFlags.none()
.stableBehaviourPreserved())
.isTrue();
for (WebAdvancedFeature feature : WebAdvancedFeature.values()) {
boolean preserved =
dev.caskeleton.adapter.inbound.web.advanced.WebAdvancedFeatureFlags.of(feature)
.stableBehaviourPreserved();
assertThat(preserved)
.as("%s claims to leave unrelated requests alone", feature)
.isEqualTo(!feature.affectsUnrelatedRequests());
}
}
44: public boolean stableBehaviourPreserved() {
45- return enabled.stream().noneMatch(WebAdvancedFeature::affectsUnrelatedRequests);
46- }
# 롤백 검증이 대상으로 삼는 값
22: public static WebAdvancedFeatureFlags none() {
27: public static WebAdvancedFeatureFlags of(WebAdvancedFeature... features) {
그 타입을 부르는 자리 전부 :
test · WebAdvancedReleaseTest.java:21 WebAdvancedFeatureFlags flags = WebAdvancedFeatureFlags.none();
test · WebAdvancedReleaseTest.java:37 assertThat(WebAdvancedFeatureFlags.of(WebAdvancedFeature.CBOR).stableBehaviourPreserved())
test · WebAdvancedReleaseTest.java:40 WebAdvancedFeatureFlags.of(WebAdvancedFeature.MVC_VIRTUAL_THREADS)
test · WebAdvancedRollbackIT.java:88 dev.caskeleton.adapter.inbound.web.advanced.WebAdvancedFeatureFlags.none()
test · WebAdvancedRollbackIT.java:93 dev.caskeleton.adapter.inbound.web.advanced.WebAdvancedFeatureFlags.of(feature)
# 그 값을 돌리는 레인과, 속성 이름을 단언하는 시험
.github/workflows/web-advanced-nightly.yml:46: :adapter:inbound:web:webAdvancedTest
.github/workflows/web-advanced-release.yml:53: :adapter:inbound:web:webAdvancedTest
test · ResumeTokenTest.java:187 assertThat(WebSocketAdvancedCapability.RESUME.propertyName())
main · WebSocketAdvancedCapability.java:75 public String propertyName() {
test · WebAdvancedReleaseTest.java:48 assertThat(WebAdvancedFeature.JSON_MERGE_PATCH.propertyName())
main · VirtualThreadProfile.java:75 public static String propertyName() {
main · WebAdvancedFeature.java:60 public String propertyName() {