Files
document-haness/docs/clean-architecture-backend-template/final/evidence/rendered/a05-f014-collection-fetch-pagination-probe.svg
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

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 &gt; /tmp/probe/cp.sh &lt;&lt;&#x27;CPSH&#x27;
# 락파일의 한 구성에 실린 좌표 전부를 캐시 경로로 바꾼다. 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=&quot;&quot;
while IFS= read -r line; do
coord=${line%%=*}; confs</tspan><tspan x="24" dy="22">=${line#*=}
case &quot;,$confs,&quot; in *&quot;,$CONF,&quot;*) ;; *) continue ;; esac
group=${coord%%:*}; rest=${coord#*:}; name=${rest</tspan><tspan x="24" dy="22">%%:*}; ver=${rest##*:}
jar=$(find &quot;$CACHE/$group/$name/$ver&quot; -name &#x27;*.jar&#x27; ! -name &#x27;*sources*&#x27; ! -name &#x27;*javadoc*&#x27; 2&gt;/</tspan><tspan x="24" dy="22">dev/null | head -1)
[ -n &quot;$jar&quot; ] &amp;&amp; cp=&quot;$cp:$jar&quot;
done &lt; &lt;(grep -E &#x27;^[a-zA-Z0-9._-]+:[^=]+=&#x27; &quot;$LOCK&quot;)
echo &quot;${cp#:}&quot;
</tspan><tspan x="24" dy="22">CPSH
cd src
CP=$(bash /tmp/probe/cp.sh)
echo &#x27;# 락파일이 고정한 제공자&#x27;
echo &quot;$CP&quot; | tr : &#x27;\n&#x27; | grep -E &#x27;hibernate-core</tspan><tspan x="24" dy="22">|/h2-&#x27; | sed &#x27;s|.*/||&#x27;
echo -n &#x27;# 저장소에서 fail_on_pagination_over_collection_fetch 를 켜는 곳: &#x27;
grep -rn &#x27;fail_on_</tspan><tspan x="24" dy="22">pagination&#x27; --include=*.java --include=*.yml --include=*.yaml --include=*.properties --include=*.gradle --exclude-dir=bu</tspan><tspan x="24" dy="22">ild .. | wc -l
echo
echo &#x27;# 게이트와 같은 모양의 조회를 그 제공자에게 직접 시킨다&#x27;
cat &gt; /tmp/probe/FetchPageProbe.java</tspan><tspan x="24" dy="22"> &lt;&lt;&#x27;JAVA&#x27;
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 = &quot;PagedParent&quot;)
public static class Parent {
@Id @GeneratedValue(str</tspan><tspan x="24" dy="22">ategy = GenerationType.SEQUENCE) Long id;
String label;
@OneToMany(mappedBy = &quot;parent&quot;, cascade = CascadeType.AL</tspan><tspan x="24" dy="22">L) List&lt;Child&gt; children = new ArrayList&lt;&gt;();
}
@Entity(name = &quot;PagedChild&quot;)
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(&quot;hibernate.connecti</tspan><tspan x="24" dy="22">on.driver_class&quot;, &quot;org.h2.Driver&quot;);
cfg.setProperty(&quot;hibernate.connection.url&quot;, &quot;jdbc:h2:mem:fetch;DB_CLOSE_DELAY=-1</tspan><tspan x="24" dy="22">&quot;);
cfg.setProperty(&quot;hibernate.connection.username&quot;, &quot;sa&quot;);
cfg.setProperty(&quot;hibernate.hbm2ddl.auto&quot;, &quot;create-dr</tspan><tspan x="24" dy="22">op&quot;);
cfg.setProperty(&quot;hibernate.generate_statistics&quot;, &quot;true&quot;);
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 -&gt; {
for (int p = 0; p &lt; 50; p++) {
Parent parent = new Parent();
parent.label = &quot;p</tspan><tspan x="24" dy="22">arent-&quot; + p;
for (int c = 0; c &lt; 4; c++) {
Child child = new Child();
child.label = &quot;c</tspan><tspan x="24" dy="22">hild-&quot; + p + &#x27;-&#x27; + 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&lt;Parent&gt; page = factory.fromTransaction(session -&gt;
session.createQuery(
&quot;select disti</tspan><tspan x="24" dy="22">nct p from PagedParent p left join fetch p.children order by p.id&quot;,
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(&quot;반환된 부모 수 : &quot; + page.size());
System.out.println(&quot;</tspan><tspan x="24" dy="22"> 조회가 낸 프리페어드 스테이트먼트 : &quot; + statements);
System.out.println(&quot;fail_on_pagination_over_collection_fetch</tspan><tspan x="24" dy="22"> : &quot;
+ factory.getSessionFactoryOptions().isFailOnPaginationOverCollectionFetchEnabled());
}
}
}
JAVA
ja</tspan><tspan x="24" dy="22">vac -encoding UTF-8 -nowarn -cp &quot;$CP&quot; -d /tmp/probe /tmp/probe/FetchPageProbe.java 2&gt;&amp;1 | grep -v &#x27;^Note:&#x27;
java -Dstdout</tspan><tspan x="24" dy="22">.encoding=UTF-8 -cp &quot;$CP:/tmp/probe&quot; FetchPageProbe 2&gt;&amp;1 \
| grep -E &#x27;HHH90003004|반환된|스테이트먼트|fail_on&#x27; | sed &#x27;</tspan><tspan x="24" dy="22">s/^[0-9:.]* \[main\] //&#x27;
echo
echo &#x27;# 서버 없이 매 빌드마다 도는 레인에도 같은 형태가 있다&#x27;
sed -n &#x27;11,19p&#x27; 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(&quot;one collection page requires a database-level parent bound&quot;)</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>