Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/analysis-finding-a06-f008.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

215 lines
17 KiB
Plaintext

# 이 타입이 존재하는 이유
BoundScopedOperations.java:12 /**
BoundScopedOperations.java:13 * {@link ScopedMongoOperations} over one physical collection.
BoundScopedOperations.java:14 *
BoundScopedOperations.java:15 * <p>Every method passes the collection this scope was built with. That is the whole implementation
BoundScopedOperations.java:16 * and the whole point: a caller cannot supply a different one, so the registered collection profile
BoundScopedOperations.java:17 * and any tenant boundary derived from it hold without the caller having to cooperate.
BoundScopedOperations.java:18 *
BoundScopedOperations.java:19 * <p>Every query-shaped method also carries the operation's deadline as {@code maxTimeMS}, and that
BoundScopedOperations.java:20 * is the difference between a deadline and a report about one. The blocking executor could only
BoundScopedOperations.java:21 * measure elapsed time after the callback returned — a Java callback cannot be interrupted mid
BoundScopedOperations.java:22 * driver call — so an operation that ran past its budget was detected, never stopped. Sent to the
BoundScopedOperations.java:23 * server, the same number ends the work.
BoundScopedOperations.java:24 *
BoundScopedOperations.java:25 * <p>It applies to the methods that take a {@link Query} or an {@link Aggregation}, because those
BoundScopedOperations.java:26 * are the ones the server can cut short. {@code insert} carries no query to attach it to.
BoundScopedOperations.java:27 */
그 데드라인이 실제로 붙는 자리 :
BoundScopedOperations.java:19 * <p>Every query-shaped method also carries the operation's deadline as {@code maxTimeMS}, and that
BoundScopedOperations.java:53 return query.maxTimeMsec(timeout.toMillis());
BoundScopedOperations.java:98 .maxTime(timeout)
BoundScopedOperations.java:48 * <p>Mutates the caller's query rather than copying it, which is what Spring Data's own fluent
BoundScopedOperations.java:49 * API does; a copy would silently drop any hint, collation or read preference the caller set.
BoundScopedOperations.java:50 */
BoundScopedOperations.java:51 private Query bounded(Query query) {
BoundScopedOperations.java:52 Objects.requireNonNull(query, "query");
BoundScopedOperations.java:53 return query.maxTimeMsec(timeout.toMillis());
BoundScopedOperations.java:54 }
BoundScopedOperations.java:55
BoundScopedOperations.java:56 @Override
BoundScopedOperations.java:92
BoundScopedOperations.java:93 @Override
BoundScopedOperations.java:94 public <T> List<T> aggregate(Aggregation aggregation, Class<T> resultType) {
BoundScopedOperations.java:95 Aggregation bounded =
BoundScopedOperations.java:96 aggregation.withOptions(
BoundScopedOperations.java:97 org.springframework.data.mongodb.core.aggregation.AggregationOptions.builder()
BoundScopedOperations.java:98 .maxTime(timeout)
BoundScopedOperations.java:99 .build());
BoundScopedOperations.java:100 return operations.aggregate(bounded, collection, resultType).getMappedResults();
BoundScopedOperations.java:101 }
BoundScopedOperations.java:102 }
# 그 타입에 닿는 유일한 문
MongoCollectionAccess.java:24
MongoCollectionAccess.java:25 /**
MongoCollectionAccess.java:26 * The operations this callback may perform.
MongoCollectionAccess.java:27 *
MongoCollectionAccess.java:28 * <p>Scoped: no method takes a collection name, so the check {@link #collection(String)} performs
MongoCollectionAccess.java:29 * cannot be bypassed by not calling it. The previous {@code MongoOperations} accessor could reach
MongoCollectionAccess.java:30 * any collection in the database.
MongoCollectionAccess.java:31 */
MongoCollectionAccess.java:32 ScopedMongoOperations scoped();
MongoCollectionAccess.java:33 }
MongoPlatformCollectionAccess.java:5 /**
MongoPlatformCollectionAccess.java:6 * Raw access, for the platform's own executors only.
MongoPlatformCollectionAccess.java:7 *
MongoPlatformCollectionAccess.java:8 * <p>The bulk, atomic, aggregation and geospatial executors build driver-level commands and need
MongoPlatformCollectionAccess.java:9 * the unrestricted template. They are inside the platform and are the code that installs the
MongoPlatformCollectionAccess.java:10 * guardrails, which is a different position from a caller's callback — so the raw handle is a type
MongoPlatformCollectionAccess.java:11 * they ask for by name rather than something every callback receives.
MongoPlatformCollectionAccess.java:12 *
MongoPlatformCollectionAccess.java:13 * <p>Keeping it out of {@link MongoCollectionAccess} is the point. An interface that offers both a
MongoPlatformCollectionAccess.java:14 * scoped API and an escape hatch offers an escape hatch.
MongoPlatformCollectionAccess.java:15 */
MongoPlatformCollectionAccess.java:16 public interface MongoPlatformCollectionAccess extends MongoCollectionAccess {
MongoPlatformCollectionAccess.java:17
MongoPlatformCollectionAccess.java:18 /** The template bound to the operation's consistency profile, with no collection scoping. */
MongoPlatformCollectionAccess.java:19 MongoOperations rawOperations();
MongoPlatformCollectionAccess.java:20 }
# 두 접근자를 부르는 자리 전수
저장소 전체에서 scoped() 를 부르는 줄 : 0 개
저장소 전체에서 rawOperations() 를 부르는 줄 : 7 개
두 이름이 나오는 자리 전부 (선언 포함) :
main · SpringMongoGeospatialOperations.java:58 return access.rawOperations().find(near, documentType, access.collection());
main · SpringMongoGeospatialOperations.java:82 return access.rawOperations().find(within, documentType, access.collection());
main · DefaultMongoImperativeExecutor.java:184 public ScopedMongoOperations scoped() {
main · DefaultMongoImperativeExecutor.java:192 public MongoOperations rawOperations() {
main · MongoCollectionAccess.java:32 ScopedMongoOperations scoped();
main · MongoPlatformCollectionAccess.java:19 MongoOperations rawOperations();
main · MongoAtomicOperationsTemplate.java:84 access.rawOperations(),
main · MongoAtomicOperationsTemplate.java:92 access.rawOperations(),
main · MongoBulkExecutor.java:69 access.rawOperations().bulkOps(plan.mode().toSpringMode(), access.collection());
main · DefaultReactiveMongoExecutor.java:218 public ReactiveScopedMongoOperations scoped() {
main · ReactiveMongoCollectionAccess.java:31 ReactiveScopedMongoOperations scoped();
test · MongoRawAccessBoundaryTest.java:18 * <p>{@code MongoPlatformCollectionAccess.rawOperations()} and {@code
test · MongoRawAccessBoundaryTest.java:37 List.of(".rawOperations()", ".executeInternal(");
[대조] 그 검색이 훑은 파일 : 6444 개
# rawOperations 가 무엇을 돌려주는가
DefaultMongoImperativeExecutor.java:180 return physicalCollection;
DefaultMongoImperativeExecutor.java:181 }
DefaultMongoImperativeExecutor.java:182
DefaultMongoImperativeExecutor.java:183 @Override
DefaultMongoImperativeExecutor.java:184 public ScopedMongoOperations scoped() {
DefaultMongoImperativeExecutor.java:185 // The deadline travels with the scope. The executor can only measure elapsed time after the
DefaultMongoImperativeExecutor.java:186 // callback returns; the scoped operations send the same number to the server as maxTimeMS,
DefaultMongoImperativeExecutor.java:187 // where it actually ends the work.
DefaultMongoImperativeExecutor.java:188 return new BoundScopedOperations(physicalCollection, operations, timeout);
DefaultMongoImperativeExecutor.java:189 }
DefaultMongoImperativeExecutor.java:190
DefaultMongoImperativeExecutor.java:191 @Override
DefaultMongoImperativeExecutor.java:192 public MongoOperations rawOperations() {
DefaultMongoImperativeExecutor.java:193 return operations;
DefaultMongoImperativeExecutor.java:194 }
DefaultMongoImperativeExecutor.java:195 }
DefaultMongoImperativeExecutor.java:196 }
# 서버에 데드라인을 실제로 붙이는 다른 경로들
PolicyAwareMongoAggregationExecutor.java:88
PolicyAwareMongoAggregationExecutor.java:89 private static MongoOperationBudget budgetFor(
PolicyAwareMongoAggregationExecutor.java:90 MongoOperationContext context, MongoOperationBudget registered) {
PolicyAwareMongoAggregationExecutor.java:91 // The context's timeout is the caller's deadline; it may tighten maxTimeMS but never extend it.
PolicyAwareMongoAggregationExecutor.java:92 long contextMillis = Math.max(1L, context.timeout().toMillis());
PolicyAwareMongoAggregationExecutor.java:93 return new MongoOperationBudget(
PolicyAwareMongoAggregationExecutor.java:94 registered.maxResults(),
PolicyAwareMongoAggregationExecutor.java:95 registered.maxResultBytes(),
PolicyAwareMongoAggregationExecutor.java:96 Math.min(registered.maxTimeMillis(), contextMillis),
PolicyAwareMongoAggregationExecutor.java:97 registered.cursorBatchSize());
PolicyAwareMongoAggregationExecutor.java:98 }
PolicyAwareMongoAggregationExecutor.java:99
PolicyAwareMongoAggregationExecutor.java:100 private static AggregationOptions optionsFor(
PolicyAwareMongoAggregationExecutor.java:101 MongoAggregationProfile profile, MongoOperationBudget budget) {
PolicyAwareMongoAggregationExecutor.java:102 AggregationOptions.Builder options =
PolicyAwareMongoAggregationExecutor.java:103 AggregationOptions.builder()
PolicyAwareMongoAggregationExecutor.java:104 .allowDiskUse(profile.allowDiskUse())
PolicyAwareMongoAggregationExecutor.java:105 .cursorBatchSize(budget.cursorBatchSize())
PolicyAwareMongoAggregationExecutor.java:106 .maxTime(Duration.ofMillis(budget.maxTimeMillis()));
PolicyAwareMongoAggregationExecutor.java:107 return profile.strictMapping() ? options.strictMapping().build() : options.build();
PolicyAwareMongoAggregationExecutor.java:108 }
maxTimeMillis 를 부르는 자리 전부 (에러 메시지 포함) :
PolicyAwareMongoAggregationExecutor.java:96 Math.min(registered.maxTimeMillis(), contextMillis),
PolicyAwareMongoAggregationExecutor.java:106 .maxTime(Duration.ofMillis(budget.maxTimeMillis()));
PolicyAwareMongoQueryBuilder.java:200 query.maxTimeMsec(budget.maxTimeMillis());
MongoBudgetEnforcer.java:36 + requested.maxTimeMillis()
MongoBudgetEnforcer.java:44 + registered.maxTimeMillis()
MongoReactiveCursorPublisher.java:58 .maxTimeMsec(budget.maxTimeMillis())
context.timeout() 이나 narrowedTo 가 나오는 자리 전부 :
PolicyAwareMongoAggregationExecutor.java:72 enforcer.narrow(budget, budget.narrowedTo(budgetFor(context, budget)));
PolicyAwareMongoAggregationExecutor.java:92 long contextMillis = Math.max(1L, context.timeout().toMillis());
DefaultMongoImperativeExecutor.java:83 ScopedAccess access = new ScopedAccess(physicalCollection, operations, context.timeout());
DefaultMongoImperativeExecutor.java:93 if (elapsed.compareTo(context.timeout()) > 0) {
DefaultMongoImperativeExecutor.java:103 + context.timeout().toMillis()
MongoBudgetEnforcer.java:57 return registered.narrowedTo(requested);
MongoOperationBudget.java:43 public MongoOperationBudget narrowedTo(MongoOperationBudget other) {
DefaultReactiveMongoExecutor.java:76 .timeout(context.timeout())
DefaultReactiveMongoExecutor.java:108 .timeout(context.timeout())
DefaultReactiveMongoExecutor.java:154 context.operationName(), operationType, context.timeout()),
# 그 다섯 자리를 가진 세 실행기가 조립되는가
MongoPlatformAutoConfiguration.java:154 .MongoAtomicOperationsTemplate(executor, policies);
MongoPlatformAutoConfiguration.java:169 return new dev.caskeleton.adapter.outbound.mongo.imperative.bulk.MongoBulkExecutor(
SpringMongoGeospatialOperations.java:31 public SpringMongoGeospatialOperations(
MongoAtomicOperationsTemplate.java:27 public MongoAtomicOperationsTemplate(
MongoBulkExecutor.java:43 public MongoBulkExecutor(
그 셋이 자기 안에서 데드라인을 따로 거는가 :
SpringMongoGeospatialOperations.java maxTime 이나 timeout 이 나오는 줄 : 0 개
MongoAtomicOperationsTemplate.java maxTime 이나 timeout 이 나오는 줄 : 0 개
MongoBulkExecutor.java maxTime 이나 timeout 이 나오는 줄 : 0 개
[대조] 같은 검색을 BoundScopedOperations 에 걸면 : 8 줄
# 저장소가 그 손잡이를 어떻게 지키는가
MongoRawAccessBoundaryTest.java:15 /**
MongoRawAccessBoundaryTest.java:16 * The Mongo platform's raw escape hatches stay inside the platform.
MongoRawAccessBoundaryTest.java:17 *
MongoRawAccessBoundaryTest.java:18 * <p>{@code MongoPlatformCollectionAccess.rawOperations()} and {@code
MongoRawAccessBoundaryTest.java:19 * DefaultMongoImperativeExecutor.executeInternal(...)} hand out Spring Data's unbounded {@code
MongoRawAccessBoundaryTest.java:20 * MongoOperations} — no collection binding, no consistency template, no deadline, no result budget.
MongoRawAccessBoundaryTest.java:21 * The scoped API exists precisely so a caller cannot reach those, and the only thing stopping one
MongoRawAccessBoundaryTest.java:22 * was a javadoc sentence saying "not public API for callers".
MongoRawAccessBoundaryTest.java:23 *
MongoRawAccessBoundaryTest.java:24 * <p>Both are genuinely needed *inside* the platform: the geospatial, atomic and bulk operations
MongoRawAccessBoundaryTest.java:25 * are built on them. So the rule is not "delete them" but "nobody outside may call them", which is
MongoRawAccessBoundaryTest.java:26 * a boundary rather than a visibility problem — the callers live in sibling packages, so Java's
MongoRawAccessBoundaryTest.java:27 * package-private cannot express it.
MongoRawAccessBoundaryTest.java:28 *
MongoRawAccessBoundaryTest.java:29 * <p>Checked from the composition root because that is the only place that sees both the platform
MongoRawAccessBoundaryTest.java:30 * and everything that consumes it. A rule living inside the Mongo leaf could only ever prove the
MongoRawAccessBoundaryTest.java:31 * leaf does not violate itself.
MongoRawAccessBoundaryTest.java:32 */
MongoRawAccessBoundaryTest.java:35 /** The escape hatches, by the text a caller would have to write to use one. */
MongoRawAccessBoundaryTest.java:36 private static final List<String> ESCAPE_HATCHES =
MongoRawAccessBoundaryTest.java:37 List.of(".rawOperations()", ".executeInternal(");
MongoRawAccessBoundaryTest.java:38
MongoRawAccessBoundaryTest.java:39 /** Where they may be used: the Mongo adapter's own production sources. */
MongoRawAccessBoundaryTest.java:40 private static final String PLATFORM_SOURCE =
MongoRawAccessBoundaryTest.java:41 "/src/adapter/outbound/persistence-mongo/src/main/java/";
# 세 실행기가 대신 하는 것
DefaultMongoImperativeExecutor.java:85 long startedAt = System.nanoTime();
DefaultMongoImperativeExecutor.java:86 try (MongoOperationObservation observation = observer.start(context, operationType)) {
DefaultMongoImperativeExecutor.java:87 try {
DefaultMongoImperativeExecutor.java:88 T value = callback.doInMongo(access);
DefaultMongoImperativeExecutor.java:89 Duration elapsed = Duration.ofNanos(System.nanoTime() - startedAt);
DefaultMongoImperativeExecutor.java:90 // A read is not a confirmed write. Recording every successful FIND as WRITE_CONFIRMED made
DefaultMongoImperativeExecutor.java:91 // the metric that answers "are writes being acknowledged" count reads as well.
DefaultMongoImperativeExecutor.java:92 MongoExecutionOutcome outcome = MongoCompletion.successOutcomeFor(operationType);
DefaultMongoImperativeExecutor.java:93 if (elapsed.compareTo(context.timeout()) > 0) {
DefaultMongoImperativeExecutor.java:94 // The context declares a deadline on every operation and nothing compared anything to it.
DefaultMongoImperativeExecutor.java:95 // A callback cannot be interrupted mid-driver-call, so this cannot cut the work short —
DefaultMongoImperativeExecutor.java:96 // but reporting a breach beats reporting success for work that ran past its budget.
DefaultMongoImperativeExecutor.java:97 observation.success(outcome);
DefaultMongoImperativeExecutor.java:98 throw MongoOperationRejectedException.of(
DefaultMongoImperativeExecutor.java:99 context.operationName().value(),
DefaultMongoImperativeExecutor.java:100 "the operation ran for "
DefaultMongoImperativeExecutor.java:101 + elapsed.toMillis()
DefaultMongoImperativeExecutor.java:102 + "ms against a declared timeout of "
DefaultMongoImperativeExecutor.java:103 + context.timeout().toMillis()
DefaultMongoImperativeExecutor.java:104 + "ms");
DefaultMongoImperativeExecutor.java:105 }
DefaultMongoImperativeExecutor.java:106 observation.success(outcome);