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>
159 lines
14 KiB
XML
159 lines
14 KiB
XML
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1734" viewBox="0 0 1200 1734" role="img">
|
|
<title>terminal evidence</title>
|
|
<desc>Terminal-style rendering generated from retained command output. Sensitive-looking values are redacted in the visual asset.</desc>
|
|
<rect x="1" y="1" width="1198" height="1732" rx="14" fill="#0d1117" stroke="#30363d"/>
|
|
<rect x="1" y="1" width="1198" height="44" rx="14" fill="#161b22"/>
|
|
<rect x="1" y="30" width="1198" height="14" fill="#161b22"/>
|
|
<circle cx="24" cy="22" r="6" fill="#ff5f57"/>
|
|
<circle cx="44" cy="22" r="6" fill="#febc2e"/>
|
|
<circle cx="64" cy="22" r="6" fill="#28c840"/>
|
|
<text x="92" y="27" fill="#8b949e" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">terminal evidence</text>
|
|
<text x="24" y="68" fill="#c9d1d9" font-size="15" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">$ set -e
|
|
set -o pipefail
|
|
D=$(mktemp -d); trap 'rm -rf "$D"' EXIT
|
|
C=/shared/codebase/clean-architecture-backend-template
|
|
</tspan><tspan x="24" dy="22">M=$C/src/adapter/inbound/web
|
|
CACHE=/root/.gradle/caches/modules-2/files-2.1
|
|
java -version 2>&1 | grep version
|
|
cp=""
|
|
whil</tspan><tspan x="24" dy="22">e IFS= read -r line; do
|
|
coord=${line%%=*}; confs=${line#*=}
|
|
case ",$confs," in *",testRuntimeClasspath,"*) ;; *) con</tspan><tspan x="24" dy="22">tinue ;; esac
|
|
g=${coord%%:*}; rest=${coord#*:}; n=${rest%%:*}; v=${rest##*:}
|
|
jar=$(find "$CACHE/$g/$n/$v" -name '*.j</tspan><tspan x="24" dy="22">ar' ! -name '*sources*' ! -name '*javadoc*' 2>/dev/null | head -1) || true
|
|
[ -n "$jar" ] && cp="$cp:$jar"
|
|
done < <(gre</tspan><tspan x="24" dy="22">p -E '^[a-zA-Z0-9._-]+:[^=]+=' "$M/gradle.lockfile")
|
|
SIB=$(find "$C/src" -path '*/build/libs/*+21234e38cdb9*.jar' ! -nam</tspan><tspan x="24" dy="22">e '*-testkit.jar' | sort | tr '\n' ':')
|
|
cat > "$D/ConverterMapperProbe.java" <<'JAVA'
|
|
import java.util.Map;
|
|
import org.s</tspan><tspan x="24" dy="22">pringframework.boot.autoconfigure.AutoConfigurations;
|
|
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTe</tspan><tspan x="24" dy="22">st;
|
|
import org.springframework.http.converter.HttpMessageConverter;
|
|
import tools.jackson.databind.ObjectMapper;
|
|
import t</tspan><tspan x="24" dy="22">ools.jackson.databind.json.JsonMapper;
|
|
|
|
/** 이 저장소의 엄격 매퍼가 실제로 요청 본문을 읽는 컨버터에 도달하는지 본다. */</tspan><tspan x="24" dy="22">
|
|
public final class ConverterMapperProbe {
|
|
|
|
/** 엄격 프로파일이 세운 파서인지 두 값으로 가른다. */
|
|
private static St</tspan><tspan x="24" dy="22">ring shape(ObjectMapper mapper) {
|
|
var c = mapper.tokenStreamFactory().streamReadConstraints();
|
|
return "maxNestin</tspan><tspan x="24" dy="22">gDepth=" + c.getMaxNestingDepth()
|
|
+ " maxStringLength=" + c.getMaxStringLength();
|
|
}
|
|
|
|
public static void main</tspan><tspan x="24" dy="22">(String[] args) throws Exception {
|
|
var runner = new org.springframework.boot.test.context.runner.WebApplicationConte</tspan><tspan x="24" dy="22">xtRunner()
|
|
.withConfiguration(AutoConfigurations.of(
|
|
Class.forName("org.springframework.boot.jackson</tspan><tspan x="24" dy="22">.autoconfigure.JacksonAutoConfiguration"),
|
|
Class.forName("org.springframework.boot.http.converter.autoconfig</tspan><tspan x="24" dy="22">ure.HttpMessageConvertersAutoConfiguration"),
|
|
Class.forName("org.springframework.boot.webmvc.autoconfigure.W</tspan><tspan x="24" dy="22">ebMvcAutoConfiguration"),
|
|
Class.forName("dev.caskeleton.adapter.inbound.web.mvc.autoconfigure.WebMvcPlatform</tspan><tspan x="24" dy="22">AutoConfiguration")));
|
|
|
|
var withoutRepo = new org.springframework.boot.test.context.runner.WebApplicationContextRunn</tspan><tspan x="24" dy="22">er()
|
|
.withConfiguration(AutoConfigurations.of(
|
|
Class.forName("org.springframework.boot.jackson.autoc</tspan><tspan x="24" dy="22">onfigure.JacksonAutoConfiguration"),
|
|
Class.forName("org.springframework.boot.http.converter.autoconfigure.Ht</tspan><tspan x="24" dy="22">tpMessageConvertersAutoConfiguration"),
|
|
Class.forName("org.springframework.boot.webmvc.autoconfigure.WebMvcA</tspan><tspan x="24" dy="22">utoConfiguration")));
|
|
|
|
runner.run(context -> {
|
|
System.out.println();
|
|
System.out.println("[컨텍스트에 있</tspan><tspan x="24" dy="22">는 JsonMapper 빈]");
|
|
Map<String, JsonMapper> mappers = context.getBeansOfType(JsonMapper.class);
|
|
for (var en</tspan><tspan x="24" dy="22">try : mappers.entrySet()) {
|
|
boolean primary = context.getBeanFactory()
|
|
.getBeanDefinition(entry.getK</tspan><tspan x="24" dy="22">ey()).isPrimary();
|
|
System.out.printf(" %-26s %s%s%n", entry.getKey(), shape(entry.getValue()),
|
|
prim</tspan><tspan x="24" dy="22">ary ? " (primary)" : "");
|
|
}
|
|
System.out.println();
|
|
System.out.println("[본문을 읽는 컨버터가 실제로 든</tspan><tspan x="24" dy="22"> 매퍼]");
|
|
var adapter = context.getBean(
|
|
org.springframework.web.servlet.mvc.method.annotation.RequestMa</tspan><tspan x="24" dy="22">ppingHandlerAdapter.class);
|
|
for (HttpMessageConverter<?> converter : adapter.getMessageConverters()) {
|
|
if </tspan><tspan x="24" dy="22">(!converter.getClass().getSimpleName().contains("Jackson")) {
|
|
continue;
|
|
}
|
|
// 컨버터가 실제로 </tspan><tspan x="24" dy="22">들고 있는 매퍼를 필드에서 꺼낸다. 접근자 이름이 판마다 다르다.
|
|
for (Class<?> type = converter.getClass(); type !</tspan><tspan x="24" dy="22">= null; type = type.getSuperclass()) {
|
|
for (var field : type.getDeclaredFields()) {
|
|
if (!ObjectMap</tspan><tspan x="24" dy="22">per.class.isAssignableFrom(field.getType())) {
|
|
continue;
|
|
}
|
|
field.setAccessible(tru</tspan><tspan x="24" dy="22">e);
|
|
Object held = field.get(converter);
|
|
if (held != null) {
|
|
System.out.println(" </tspan><tspan x="24" dy="22">" + converter.getClass().getSimpleName()
|
|
+ " " + shape((ObjectMapper) held));
|
|
}
|
|
</tspan><tspan x="24" dy="22"> }
|
|
}
|
|
}
|
|
System.out.println();
|
|
System.out.println("[엄격 프로파일이 세우는 파서]");
|
|
var st</tspan><tspan x="24" dy="22">rict = dev.caskeleton.adapter.inbound.web.json.BoundedJsonFactory.create(
|
|
dev.caskeleton.adapter.inbound.web.j</tspan><tspan x="24" dy="22">son.WebJsonProfile.strict());
|
|
var sc = strict.streamReadConstraints();
|
|
System.out.println(" maxNestingDepth</tspan><tspan x="24" dy="22">=" + sc.getMaxNestingDepth()
|
|
+ " maxStringLength=" + sc.getMaxStringLength());
|
|
});
|
|
|
|
// 대조군. 이 저장</tspan><tspan x="24" dy="22">소 자동설정만 빼면 무엇이 오는지 본다.
|
|
withoutRepo.run(context -> {
|
|
System.out.println();
|
|
System.out.pri</tspan><tspan x="24" dy="22">ntln("[이 저장소 자동설정을 뺀 같은 컨텍스트]");
|
|
for (var entry : context.getBeansOfType(JsonMapper.class).entrySe</tspan><tspan x="24" dy="22">t()) {
|
|
System.out.printf(" %-26s %s%n", entry.getKey(), shape(entry.getValue()));
|
|
}
|
|
var adapter = c</tspan><tspan x="24" dy="22">ontext.getBean(
|
|
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.class);
|
|
</tspan><tspan x="24" dy="22"> for (HttpMessageConverter<?> converter : adapter.getMessageConverters()) {
|
|
if (!converter.getClass().getSimpl</tspan><tspan x="24" dy="22">eName().equals("JacksonJsonHttpMessageConverter")) {
|
|
continue;
|
|
}
|
|
for (Class<?> type = converte</tspan><tspan x="24" dy="22">r.getClass(); type != null; type = type.getSuperclass()) {
|
|
for (var field : type.getDeclaredFields()) {
|
|
</tspan><tspan x="24" dy="22"> if (!ObjectMapper.class.isAssignableFrom(field.getType())) {
|
|
continue;
|
|
}
|
|
fie</tspan><tspan x="24" dy="22">ld.setAccessible(true);
|
|
Object held = field.get(converter);
|
|
if (held != null) {
|
|
Sy</tspan><tspan x="24" dy="22">stem.out.println(" " + converter.getClass().getSimpleName()
|
|
+ " " + shape((ObjectMapper) held));
|
|
</tspan><tspan x="24" dy="22"> }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
JAVA
|
|
javac -encoding UTF-8 -cp "$SIB$cp" -d "$D/out" "$D/Converte</tspan><tspan x="24" dy="22">rMapperProbe.java" 2>&1 | head -20
|
|
printf '%s\n' '<configuration><root level="OFF"/></configuration>' > "$D/out/logback.</tspan><tspan x="24" dy="22">xml"
|
|
java -Dstdout.encoding=UTF-8 -cp "$D/out:$SIB$cp" ConverterMapperProbe
|
|
</tspan></text>
|
|
<text x="24" y="1212" fill="#8b949e" font-size="13" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">cwd: /shared/codebase/clean-architecture-backend-template/src</tspan></text>
|
|
<text x="24" y="1234" fill="#8b949e" font-size="13" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">time: 2026-09-03T01:52:47+00:00 · exit 0</tspan></text>
|
|
<line x1="24" y1="1248" x2="1176" y2="1248" stroke="#30363d"/>
|
|
<text x="24" y="1288" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">openjdk version "21.0.12" 2026-07-21</tspan></text>
|
|
<text x="24" y="1310" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">warning: unknown enum constant Status.STABLE</tspan></text>
|
|
<text x="24" y="1332" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> reason: class file for org.apiguardian.api.API$Status not found</tspan></text>
|
|
<text x="24" y="1354" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">1 warning</tspan></text>
|
|
<text x="24" y="1376" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
|
|
<text x="24" y="1398" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[컨텍스트에 있는 JsonMapper 빈]</tspan></text>
|
|
<text x="24" y="1420" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> webStrictObjectMapper maxNestingDepth=64 maxStringLength=1048576</tspan></text>
|
|
<text x="24" y="1442" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
|
|
<text x="24" y="1464" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[본문을 읽는 컨버터가 실제로 든 매퍼]</tspan></text>
|
|
<text x="24" y="1486" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> JacksonJsonHttpMessageConverter maxNestingDepth=64 maxStringLength=1048576</tspan></text>
|
|
<text x="24" y="1508" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> JacksonCborHttpMessageConverter maxNestingDepth=500 maxStringLength=100000000</tspan></text>
|
|
<text x="24" y="1530" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> JacksonXmlHttpMessageConverter maxNestingDepth=500 maxStringLength=100000000</tspan></text>
|
|
<text x="24" y="1552" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
|
|
<text x="24" y="1574" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[엄격 프로파일이 세우는 파서]</tspan></text>
|
|
<text x="24" y="1596" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> maxNestingDepth=64 maxStringLength=1048576</tspan></text>
|
|
<text x="24" y="1618" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
|
|
<text x="24" y="1640" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[이 저장소 자동설정을 뺀 같은 컨텍스트]</tspan></text>
|
|
<text x="24" y="1662" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> jacksonJsonMapper maxNestingDepth=500 maxStringLength=100000000</tspan></text>
|
|
<text x="24" y="1684" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> JacksonJsonHttpMessageConverter maxNestingDepth=500 maxStringLength=100000000</tspan></text>
|
|
</svg>
|