Files
document-haness/docs/clean-architecture-backend-template/final/evidence/rendered/a13-f004-thymeleaf-messages.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

249 lines
23 KiB
XML

<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="2636" viewBox="0 0 1200 2636" 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="2634" 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 &#x27;rm -rf &quot;$D&quot;&#x27; EXIT
C=/shared/codebase/clean-architecture-backend-template
</tspan><tspan x="24" dy="22">N=$C/src/adapter/outbound/notification
CACHE=/root/.gradle/caches/modules-2/files-2.1
java -version 2&gt;&amp;1 | head -1
cp=&quot;&quot;</tspan><tspan x="24" dy="22">
while IFS= read -r line; do
coord=${line%%=*}; confs=${line#*=}
case &quot;,$confs,&quot; in *&quot;,runtimeClasspath,&quot;*) ;; *) co</tspan><tspan x="24" dy="22">ntinue ;; esac
g=${coord%%:*}; rest=${coord#*:}; n=${rest%%:*}; v=${rest##*:}
jar=$(find &quot;$CACHE/$g/$n/$v&quot; -name &#x27;*.</tspan><tspan x="24" dy="22">jar&#x27; ! -name &#x27;*sources*&#x27; ! -name &#x27;*javadoc*&#x27; 2&gt;/dev/null | head -1)
[ -n &quot;$jar&quot; ] &amp;&amp; cp=&quot;$cp:$jar&quot;
done &lt; &lt;(grep -E &#x27;^</tspan><tspan x="24" dy="22">[a-zA-Z0-9._-]+:[^=]+=&#x27; &quot;$N/gradle.lockfile&quot;)
# jackson-core 는 이 프로브가 부르지 않는 valueMasker() 를 컴파일하려고만 </tspan><tspan x="24" dy="22">붙인다.
# 여기서 재는 mask(String) 은 java.util.regex 만 쓴다.
JACKSON=$(find &quot;$CACHE/com.fasterxml.jackson.core/jackson</tspan><tspan x="24" dy="22">-core&quot; -name &#x27;jackson-core-*.jar&#x27; ! -name &#x27;*sources*&#x27; | head -1)
LOGSTASH=$(find &quot;$CACHE/net.logstash.logback/logstash-l</tspan><tspan x="24" dy="22">ogback-encoder/8.0&quot; -name &#x27;*.jar&#x27; ! -name &#x27;*sources*&#x27; | head -1)
CP=$(find &quot;$N/build/libs&quot; &quot;$C/src/application-core/buil</tspan><tspan x="24" dy="22">d/libs&quot; -name &#x27;*+21234e38cdb9.jar&#x27; | tr &#x27;\n&#x27; &#x27;:&#x27;)$JACKSON:$LOGSTASH${cp}
# 마스킹 카탈로그는 빌드 산출물이 아니라 고정 </tspan><tspan x="24" dy="22">리비전 소스에서 바로 컴파일한다.
javac -encoding UTF-8 -cp &quot;$CP&quot; -d &quot;$D&quot; \
&quot;$C/src/app-bootstrap/src/main/java/dev/cas</tspan><tspan x="24" dy="22">keleton/bootstrap/logging/LogMaskingPatterns.java&quot;
cat &gt; &quot;$D/RenderFailureProbe.java&quot; &lt;&lt;&#x27;JAVA&#x27;
import ch.qos.logback.cla</tspan><tspan x="24" dy="22">ssic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.ThrowableProxyUti</tspan><tspan x="24" dy="22">l;
import ch.qos.logback.core.read.ListAppender;
import dev.caskeleton.adapter.outbound.notification.platform.template.T</tspan><tspan x="24" dy="22">emplateSlotMode;
import dev.caskeleton.adapter.outbound.notification.platform.template.ThymeleafStringTemplateEngine;
im</tspan><tspan x="24" dy="22">port dev.caskeleton.bootstrap.logging.LogMaskingPatterns;
import java.util.HashMap;
import java.util.List;
import java.u</tspan><tspan x="24" dy="22">til.Map;
import org.slf4j.LoggerFactory;
/** 같은 식을 두 오버로드에 넣고, 호출자가 받는 것과 로그에 남는 것을 따로 잰</tspan><tspan x="24" dy="22">다. */
public final class RenderFailureProbe {
private static final String CODE = &quot;483920&quot;;
/** 값 하나가 아니라 </tspan><tspan x="24" dy="22">값 객체가 변수로 들어오는 경우를 보려고 둔 합성 레코드. */
record Recipient(String email, String code) {}
private s</tspan><tspan x="24" dy="22">tatic final Recipient RECIPIENT = new Recipient(&quot;alice@example.com&quot;, CODE);
private static final Map&lt;String, Object&gt; V</tspan><tspan x="24" dy="22">ARIABLES =
Map.of(&quot;code&quot;, CODE, &quot;recipient&quot;, RECIPIENT);
private static final ThymeleafStringTemplateEngine ENG</tspan><tspan x="24" dy="22">INE = new ThymeleafStringTemplateEngine();
private static ListAppender&lt;ILoggingEvent&gt; sink;
/** 한 번 렌더한 결과:</tspan><tspan x="24" dy="22"> 호출자가 값을 보는가, 로그에 남는가, 마스킹이 지우는가. */
private record Seen(boolean caller, boolean logged, boolea</tspan><tspan x="24" dy="22">n masked) {}
/** 원인 사슬 전체를 잇는다. 최상위 메시지만 보면 아래에 담긴 값을 놓친다. */
private static String ch</tspan><tspan x="24" dy="22">ain(Throwable failure) {
StringBuilder joined = new StringBuilder();
for (Throwable step = failure; step != null</tspan><tspan x="24" dy="22">; step = step.getCause()) {
joined.append(step.getClass().getSimpleName()).append(&quot;: &quot;).append(step.getMessage());</tspan><tspan x="24" dy="22">
}
return joined.toString();
}
/** logback 이 실제로 찍을 형태: 메시지 + 예외 사슬. */
private static Str</tspan><tspan x="24" dy="22">ing drainLog() {
StringBuilder printed = new StringBuilder();
for (ILoggingEvent event : List.copyOf(sink.list))</tspan><tspan x="24" dy="22"> {
printed.append(event.getFormattedMessage());
if (event.getThrowableProxy() != null) {
printed.app</tspan><tspan x="24" dy="22">end(ThrowableProxyUtil.asString(event.getThrowableProxy()));
}
}
sink.list.clear();
return printed.toS</tspan><tspan x="24" dy="22">tring();
}
private static boolean carries(String text) {
return text.contains(CODE) || text.contains(&quot;alice@exa</tspan><tspan x="24" dy="22">mple.com&quot;);
}
private static Seen measure(Runnable call) {
sink.list.clear();
String caught = &quot;&quot;;
try {</tspan><tspan x="24" dy="22">
call.run();
} catch (RuntimeException failure) {
caught = chain(failure);
}
String logged = dra</tspan><tspan x="24" dy="22">inLog();
return new Seen(carries(caught), carries(logged), carries(LogMaskingPatterns.mask(logged)));
}
private</tspan><tspan x="24" dy="22"> static String mark(boolean seen) {
return seen ? &quot;있음&quot; : &quot;없음&quot;;
}
/** 두 오버로드를 나란히 재고 한 줄로 적는</tspan><tspan x="24" dy="22">다. 로그 쪽이 갈리면 그렇게 적는다. */
private static void shape(String source) {
Seen plain = measure(() -&gt; ENGIN</tspan><tspan x="24" dy="22">E.render(source, VARIABLES));
Seen moded = measure(() -&gt; ENGINE.render(TemplateSlotMode.TEXT, source, VARIABLES));
</tspan><tspan x="24" dy="22"> String logged = plain.logged() == moded.logged() ? mark(moded.logged()) : &quot;두 쪽이 다름&quot;;
String masked = plain.m</tspan><tspan x="24" dy="22">asked() == moded.masked() ? mark(moded.masked()) : &quot;두 쪽이 다름&quot;;
System.out.println(source);
System.out.printf</tspan><tspan x="24" dy="22">(&quot; 호출자 모드없음 %s · 모드있음* %s · 로그 %s · 마스킹 뒤 %s%n&quot;,
mark(plain.caller()), mark(moded.caller())</tspan><tspan x="24" dy="22">, logged, masked);
}
/** OGNL 이나 JDK 가 만든 첫 절. Thymeleaf 의 포장 절은 건너뛴다. */
private static void cla</tspan><tspan x="24" dy="22">use(String label, Runnable call) {
try {
call.run();
System.out.printf(&quot; %-30s (실패하지 않음)%n&quot;, labe</tspan><tspan x="24" dy="22">l);
} catch (RuntimeException failure) {
for (Throwable step = failure; step != null; step = step.getCause()) </tspan><tspan x="24" dy="22">{
String type = step.getClass().getName();
if (!type.startsWith(&quot;ognl.&quot;) &amp;&amp; !type.startsWith(&quot;java.&quot;)) {</tspan><tspan x="24" dy="22">
continue;
}
// toString 을 재정의하지 않은 객체는 실행마다 다른 주소를 남긴다. 여기서 재는 것</tspan><tspan x="24" dy="22">
// 값이 적히는지 여부이므로 주소만 고정 표시로 바꾼다.
String message = String.valueOf(step.getMessag</tspan><tspan x="24" dy="22">e()).replaceAll(&quot;@[0-9a-f]{4,}&quot;, &quot;@&lt;주소&gt;&quot;);
System.out.printf(&quot; %-30s %s: %s%n&quot;, label, step.getClass().getSim</tspan><tspan x="24" dy="22">pleName(), message);
return;
}
}
}
private static void clauseOf(String source) {
clause(sourc</tspan><tspan x="24" dy="22">e, () -&gt; ENGINE.render(TemplateSlotMode.TEXT, source, VARIABLES));
}
public static void main(String[] args) {
L</tspan><tspan x="24" dy="22">oggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
// 기본 콘솔 어펜더를 뗀다. 남기면 재려는 </tspan><tspan x="24" dy="22">로그가 잰 결과와 섞인다.
context.getLogger(&quot;ROOT&quot;).detachAndStopAllAppenders();
sink = new ListAppender&lt;&gt;();
</tspan><tspan x="24" dy="22"> sink.setContext(context);
sink.start();
context.getLogger(&quot;ROOT&quot;).addAppender(sink);
System.out.println();</tspan><tspan x="24" dy="22">
System.out.println(&quot;code=&quot; + CODE + &quot;, recipient=&quot; + RECIPIENT + &quot; 를 넣고 렌더한다.&quot;);
System.out.println(&quot;* </tspan><tspan x="24" dy="22">표시가 프로덕션이 부르는 오버로드다.&quot;);
System.out.println();
String[] leaking = {
&quot;[[${code.substring(99</tspan><tspan x="24" dy="22">)}]]&quot;,
&quot;[[${code.noSuchMethod()}]]&quot;,
&quot;[[${recipient.noSuchMethod()}]]&quot;,
&quot;[[${recipient.email.substring</tspan><tspan x="24" dy="22">(99)}]]&quot;,
&quot;[[${#numbers.formatDecimal(code, 1, 2)}]]&quot;,
&quot;[[${code[recipient]}]]&quot;,
&quot;[[${recipient * 2}]]</tspan><tspan x="24" dy="22">&quot;,
};
String[] quiet = {
&quot;[[${#strings.substring(code, 99, 200)}]]&quot;,
&quot;[[${code.noSuchProperty}]]&quot;,
</tspan><tspan x="24" dy="22"> &quot;[[${code.length() / 0}]]&quot;,
};
System.out.println(&quot;[실패한 자리에 변수가 놓인 식]&quot;);
for (String sourc</tspan><tspan x="24" dy="22">e : leaking) {
shape(source);
}
System.out.println();
System.out.println(&quot;[실패한 자리에 변수가 없는 </tspan><tspan x="24" dy="22">식]&quot;);
for (String source : quiet) {
shape(source);
}
System.out.println(&quot;[[${code}]] (이 줄만 변수를</tspan><tspan x="24" dy="22"> 하나도 주지 않는다)&quot;);
Seen plain = measure(() -&gt; ENGINE.render(&quot;[[${code}]]&quot;, new HashMap&lt;&gt;()));
Seen moded =
</tspan><tspan x="24" dy="22"> measure(() -&gt; ENGINE.render(TemplateSlotMode.TEXT, &quot;[[${code}]]&quot;, new HashMap&lt;&gt;()));
System.out.printf(&quot;</tspan><tspan x="24" dy="22">출자 모드없음 %s · 모드있음* %s · 로그 %s · 마스킹 뒤 %s%n&quot;,
mark(plain.caller()), mark(moded.caller()), mark</tspan><tspan x="24" dy="22">(moded.logged()), mark(moded.masked()));
System.out.println();
System.out.println(&quot;[OGNL 이나 JDK 가 만든 절]&quot;)</tspan><tspan x="24" dy="22">;
for (String source : leaking) {
clauseOf(source);
}
for (String source : quiet) {
clauseOf(sou</tspan><tspan x="24" dy="22">rce);
}
}
}
JAVA
javac -encoding UTF-8 -cp &quot;$CP:$D&quot; -d &quot;$D&quot; &quot;$D/RenderFailureProbe.java&quot;
java -Dstdout.encoding=UT</tspan><tspan x="24" dy="22">F-8 -cp &quot;$CP:$D&quot; RenderFailureProbe
</tspan></text>
<text x="24" y="1542" 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="1564" 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-02T16:07:05+00:00 · exit 0</tspan></text>
<line x1="24" y1="1578" x2="1176" y2="1578" stroke="#30363d"/>
<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">openjdk version &quot;21.0.12&quot; 2026-07-21</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">code=483920, recipient=Recipient[email=alice@example.com, code=483920] 를 넣고 렌더한다.</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">* 표시가 프로덕션이 부르는 오버로드다.</tspan></text>
<text x="24" y="1706" 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="1728" 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="1750" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[[${code.substring(99)}]]</tspan></text>
<text x="24" y="1772" 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="1794" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[[${code.noSuchMethod()}]]</tspan></text>
<text x="24" y="1816" 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="1838" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[[${recipient.noSuchMethod()}]]</tspan></text>
<text x="24" y="1860" 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="1882" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[[${recipient.email.substring(99)}]]</tspan></text>
<text x="24" y="1904" 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="1926" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[[${#numbers.formatDecimal(code, 1, 2)}]]</tspan></text>
<text x="24" y="1948" 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="1970" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[[${code[recipient]}]]</tspan></text>
<text x="24" y="1992" 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="2014" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[[${recipient * 2}]]</tspan></text>
<text x="24" y="2036" 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="2058" 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="2080" 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="2102" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[[${#strings.substring(code, 99, 200)}]]</tspan></text>
<text x="24" y="2124" 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="2146" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[[${code.noSuchProperty}]]</tspan></text>
<text x="24" y="2168" 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="2190" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[[${code.length() / 0}]]</tspan></text>
<text x="24" y="2212" 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="2234" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[[${code}]] (이 줄만 변수를 하나도 주지 않는다)</tspan></text>
<text x="24" y="2256" 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="2278" 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="2300" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[OGNL 이나 JDK 가 만든 절]</tspan></text>
<text x="24" y="2322" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> [[${code.substring(99)}]] MethodFailedException: Method &quot;substring&quot; failed for object 483920</tspan></text>
<text x="24" y="2344" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> [[${code.noSuchMethod()}]] MethodFailedException: Method &quot;noSuchMethod&quot; failed for object 483920</tspan></text>
<text x="24" y="2366" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> [[${recipient.noSuchMethod()}]] MethodFailedException: Method &quot;noSuchMethod&quot; failed for object Recipient[email=alice@example.c</tspan><tspan x="24" dy="22">om, code=483920]</tspan></text>
<text x="24" y="2410" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> [[${recipient.email.substring(99)}]] MethodFailedException: Method &quot;substring&quot; failed for object alice@example.com</tspan></text>
<text x="24" y="2432" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> [[${#numbers.formatDecimal(code, 1, 2)}]] IllegalArgumentException: Unable to convert type java.lang.String of 483920 to type </tspan><tspan x="24" dy="22">of java.lang.Number</tspan></text>
<text x="24" y="2476" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> [[${code[recipient]}]] NoSuchPropertyException: java.lang.String.Recipient[email=alice@example.com, code=483920]</tspan></text>
<text x="24" y="2498" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> [[${recipient * 2}]] NumberFormatException: For input string: &quot;Recipient[email=alice@example.com, code=483920]&quot;</tspan></text>
<text x="24" y="2520" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> [[${#strings.substring(code, 99, 200)}]] MethodFailedException: Method &quot;substring&quot; failed for object org.thymeleaf.expression.</tspan><tspan x="24" dy="22">Strings@&lt;주소&gt;</tspan></text>
<text x="24" y="2564" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> [[${code.noSuchProperty}]] NoSuchPropertyException: java.lang.String.noSuchProperty</tspan></text>
<text x="24" y="2586" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> [[${code.length() / 0}]] ArithmeticException: / by zero</tspan></text>
</svg>