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>
140 lines
12 KiB
XML
140 lines
12 KiB
XML
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1470" viewBox="0 0 1200 1470" 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="1468" 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">$ mkdir -p /tmp/probe
|
|
cat > /tmp/probe/cp.sh <<'CPSH'
|
|
# 락파일의 한 구성에 실린 좌표 전부를 캐시 경로로 바꾼다. Gradle </tspan><tspan x="24" dy="22">실행 없이 재현된다.
|
|
LOCK=${1:-adapter/outbound/persistence-jpa/gradle.lockfile}
|
|
CONF=${2:-postgresqlIntegrationTestRunti</tspan><tspan x="24" dy="22">meClasspath}
|
|
CACHE=/root/.gradle/caches/modules-2/files-2.1
|
|
cp=""
|
|
while IFS= read -r line; do
|
|
coord=${line%%=*}; confs</tspan><tspan x="24" dy="22">=${line#*=}
|
|
case ",$confs," in *",$CONF,"*) ;; *) continue ;; esac
|
|
group=${coord%%:*}; rest=${coord#*:}; name=${rest</tspan><tspan x="24" dy="22">%%:*}; ver=${rest##*:}
|
|
jar=$(find "$CACHE/$group/$name/$ver" -name '*.jar' ! -name '*sources*' ! -name '*javadoc*' 2>/</tspan><tspan x="24" dy="22">dev/null | head -1)
|
|
[ -n "$jar" ] && cp="$cp:$jar"
|
|
done < <(grep -E '^[a-zA-Z0-9._-]+:[^=]+=' "$LOCK")
|
|
echo "${cp#:}"
|
|
</tspan><tspan x="24" dy="22">CPSH
|
|
cd src
|
|
CP=$(bash /tmp/probe/cp.sh)
|
|
echo '# 락파일이 고정한 제공자'
|
|
echo "$CP" | tr : '\n' | grep -E 'hibernate-core</tspan><tspan x="24" dy="22">|/h2-' | sed 's|.*/||'
|
|
echo -n '# 저장소에서 fail_on_pagination_over_collection_fetch 를 켜는 곳: '
|
|
grep -rn 'fail_on_</tspan><tspan x="24" dy="22">pagination' --include=*.java --include=*.yml --include=*.yaml --include=*.properties --include=*.gradle --exclude-dir=bu</tspan><tspan x="24" dy="22">ild .. | wc -l
|
|
|
|
echo
|
|
echo '# 게이트와 같은 모양의 조회를 그 제공자에게 직접 시킨다'
|
|
cat > /tmp/probe/FetchPageProbe.java</tspan><tspan x="24" dy="22"> <<'JAVA'
|
|
import jakarta.persistence.CascadeType;
|
|
import jakarta.persistence.Entity;
|
|
import jakarta.persistence.Generate</tspan><tspan x="24" dy="22">dValue;
|
|
import jakarta.persistence.GenerationType;
|
|
import jakarta.persistence.Id;
|
|
import jakarta.persistence.ManyToOne;
|
|
</tspan><tspan x="24" dy="22">import jakarta.persistence.OneToMany;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import org.hibernate.SessionFac</tspan><tspan x="24" dy="22">tory;
|
|
import org.hibernate.cfg.Configuration;
|
|
|
|
/** 컬렉션 페치에 페이지 상한을 걸었을 때 상한이 SQL 로 가는지 본다. */
|
|
p</tspan><tspan x="24" dy="22">ublic class FetchPageProbe {
|
|
|
|
@Entity(name = "PagedParent")
|
|
public static class Parent {
|
|
@Id @GeneratedValue(str</tspan><tspan x="24" dy="22">ategy = GenerationType.SEQUENCE) Long id;
|
|
String label;
|
|
@OneToMany(mappedBy = "parent", cascade = CascadeType.AL</tspan><tspan x="24" dy="22">L) List<Child> children = new ArrayList<>();
|
|
}
|
|
|
|
@Entity(name = "PagedChild")
|
|
public static class Child {
|
|
@Id @</tspan><tspan x="24" dy="22">GeneratedValue(strategy = GenerationType.SEQUENCE) Long id;
|
|
String label;
|
|
@ManyToOne Parent parent;
|
|
}
|
|
|
|
publi</tspan><tspan x="24" dy="22">c static void main(String[] args) {
|
|
Configuration cfg = new Configuration();
|
|
cfg.setProperty("hibernate.connecti</tspan><tspan x="24" dy="22">on.driver_class", "org.h2.Driver");
|
|
cfg.setProperty("hibernate.connection.url", "jdbc:h2:mem:fetch;DB_CLOSE_DELAY=-1</tspan><tspan x="24" dy="22">");
|
|
cfg.setProperty("hibernate.connection.username", "sa");
|
|
cfg.setProperty("hibernate.hbm2ddl.auto", "create-dr</tspan><tspan x="24" dy="22">op");
|
|
cfg.setProperty("hibernate.generate_statistics", "true");
|
|
cfg.addAnnotatedClass(Parent.class);
|
|
cfg.add</tspan><tspan x="24" dy="22">AnnotatedClass(Child.class);
|
|
|
|
try (SessionFactory factory = cfg.buildSessionFactory()) {
|
|
factory.inTransaction</tspan><tspan x="24" dy="22">(session -> {
|
|
for (int p = 0; p < 50; p++) {
|
|
Parent parent = new Parent();
|
|
parent.label = "p</tspan><tspan x="24" dy="22">arent-" + p;
|
|
for (int c = 0; c < 4; c++) {
|
|
Child child = new Child();
|
|
child.label = "c</tspan><tspan x="24" dy="22">hild-" + p + '-' + c;
|
|
child.parent = parent;
|
|
parent.children.add(child);
|
|
}
|
|
s</tspan><tspan x="24" dy="22">ession.persist(parent);
|
|
}
|
|
});
|
|
|
|
long before = factory.getStatistics().getPrepareStatementCount();
|
|
</tspan><tspan x="24" dy="22"> List<Parent> page = factory.fromTransaction(session ->
|
|
session.createQuery(
|
|
"select disti</tspan><tspan x="24" dy="22">nct p from PagedParent p left join fetch p.children order by p.id",
|
|
Parent.class)
|
|
.setMa</tspan><tspan x="24" dy="22">xResults(20)
|
|
.getResultList());
|
|
long statements = factory.getStatistics().getPrepareStatementCount()</tspan><tspan x="24" dy="22"> - before;
|
|
|
|
System.out.println("반환된 부모 수 : " + page.size());
|
|
System.out.println("그</tspan><tspan x="24" dy="22"> 조회가 낸 프리페어드 스테이트먼트 : " + statements);
|
|
System.out.println("fail_on_pagination_over_collection_fetch</tspan><tspan x="24" dy="22"> : "
|
|
+ factory.getSessionFactoryOptions().isFailOnPaginationOverCollectionFetchEnabled());
|
|
}
|
|
}
|
|
}
|
|
JAVA
|
|
ja</tspan><tspan x="24" dy="22">vac -encoding UTF-8 -nowarn -cp "$CP" -d /tmp/probe /tmp/probe/FetchPageProbe.java 2>&1 | grep -v '^Note:'
|
|
java -Dstdout</tspan><tspan x="24" dy="22">.encoding=UTF-8 -cp "$CP:/tmp/probe" FetchPageProbe 2>&1 \
|
|
| grep -E 'HHH90003004|반환된|스테이트먼트|fail_on' | sed '</tspan><tspan x="24" dy="22">s/^[0-9:.]* \[main\] //'
|
|
|
|
echo
|
|
echo '# 서버 없이 매 빌드마다 도는 레인에도 같은 형태가 있다'
|
|
sed -n '11,19p' adapter/out</tspan><tspan x="24" dy="22">bound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/testkit/fetch/FetchPaginationExpectation</tspan><tspan x="24" dy="22">Test.java</tspan></text>
|
|
<text x="24" y="904" 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</tspan></text>
|
|
<text x="24" y="926" 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-02T04:06:14+00:00 · exit 0</tspan></text>
|
|
<line x1="24" y1="940" x2="1176" y2="940" stroke="#30363d"/>
|
|
<text x="24" y="980" 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="1002" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">h2-2.4.240.jar</tspan></text>
|
|
<text x="24" y="1024" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">hibernate-core-7.2.24.Final.jar</tspan></text>
|
|
<text x="24" y="1046" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"># 저장소에서 fail_on_pagination_over_collection_fetch 를 켜는 곳: 0</tspan></text>
|
|
<text x="24" y="1068" 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="1090" 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="1112" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">WARN org.hibernate.orm.query -- HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory</tspan></text>
|
|
<text x="24" y="1134" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">반환된 부모 수 : 20</tspan></text>
|
|
<text x="24" y="1156" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">그 조회가 낸 프리페어드 스테이트먼트 : 1</tspan></text>
|
|
<text x="24" y="1178" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">fail_on_pagination_over_collection_fetch : false</tspan></text>
|
|
<text x="24" y="1200" 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="1222" 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="1244" 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="1266" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> @Test</tspan></text>
|
|
<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"> @DisplayName("one collection page requires a database-level parent bound")</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"> void oneCollectionPageRequiresBoundedParentSelection() {</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"> var expected = FetchPaginationExpectation.hibernate74PostgreSql(20);</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"></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"> assertThat(expected.maxReturnedParents()).isEqualTo(20);</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"> assertThat(expected.requiresDatabaseLimit()).isTrue();</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"> }</tspan></text>
|
|
</svg>
|